Skip to content

Commit

Permalink
Merge pull request #243 from kenellorando/patch1
Browse files Browse the repository at this point in the history
Expand logging coverage and fix panics from no Icecast data
  • Loading branch information
kenellorando authored Mar 11, 2023
2 parents 8042b22 + 2157861 commit 241543e
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 26 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ All components are mostly pre-configured so there is hardly any configuration re
3. Edit `config/liquidsoap.liq`
1. Change all instances of `hackme` to a new password.
2. If you changed `CSERVER_MUSIC_DIR` in step 1, change any instances of the default value `/music/` to match it here.
4. (_Optional_) Edit `config/nginx.conf`
4. Edit `docker-compose.yml`
1. If you changed `CSERVER_MUSIC_DIR` in step 1, change any instances of the `Volumes` defined, replacing `/music:/music` with `/YOURDIR:/music`.
5. (_Optional_) Edit `config/nginx.conf`
1. For advanced users deploying Cadence to a server with DNS, Cadence ships with a reverse proxy that can forward requests based on domain-name to backend services. Simply configure the `server_name` values with your domain names. The stream server domain should match the value you set in step 2.
5. `docker compose up`
6. `docker compose up`

After configuration is initially completed, you may simply run `docker compose up` again in the future to start your station.

Expand Down
79 changes: 70 additions & 9 deletions cadence/server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,22 @@ func Search() http.HandlerFunc {
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&search)
if err != nil {
clog.Error("Search", "Unable to decode search body.", err)
w.WriteHeader(http.StatusBadRequest) // 400 Bad Request
return
}
queryResults, err := searchByQuery(search.Query)
if err != nil {
clog.Error("Search", "Unable to execute search by query.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
jsonMarshal, err := json.Marshal(queryResults)
if err != nil {
clog.Error("Search", "Failed to marshal results from the search.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
jsonMarshal, _ := json.Marshal(queryResults)
w.Header().Set("Content-Type", "application/json")
w.Write(jsonMarshal)
}
Expand Down Expand Up @@ -68,11 +75,13 @@ func RequestID() http.HandlerFunc {
}
path, err := getPathById(reqID)
if err != nil {
clog.Error("RequestID", "Unable to find file path by song ID.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
_, err = liquidsoapRequest(path)
if err != nil {
clog.Error("RequestID", "Unable to submit song request.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
Expand All @@ -93,21 +102,25 @@ func RequestBestMatch() http.HandlerFunc {
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&rbm)
if err != nil {
clog.Error("RequestBestMatch", "Unable to decode request body.", err)
w.WriteHeader(http.StatusBadRequest) // 400 Bad Request
return
}
queryResults, err := searchByQuery(rbm.Query)
if err != nil {
clog.Error("RequestBestMatch", "Unable to search by query.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
path, err := getPathById(queryResults[0].ID)
if err != nil {
clog.Error("RequestBestMatch", "Unable to find file path by song ID", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
_, err = liquidsoapRequest(path)
if err != nil {
clog.Error("RequestBestMatch", "Unable to submit song request.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
Expand All @@ -119,12 +132,23 @@ func RequestBestMatch() http.HandlerFunc {
// Gets text metadata (excludes album art and path) of the currently playing song.
func NowPlayingMetadata() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
queryResult, err := searchByTitleArtist(now.Song.Title, now.Song.Artist)
queryResults, err := searchByTitleArtist(now.Song.Title, now.Song.Artist)
if err != nil {
clog.Error("NowPlayingMetadata", "Unable to search by title and artist.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
if len(queryResults) < 1 {
clog.Warn("NowPlayingMetadata", "The currently playing song could not be found in the database. The database may not be populated.")
w.WriteHeader(http.StatusNotFound) // 404 Not Found
return
}
jsonMarshal, err := json.Marshal(queryResults[0])
if err != nil {
clog.Error("NowPlayingMetadata", "Failed to marshal results from the search.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
jsonMarshal, _ := json.Marshal(queryResult[0])
w.Header().Set("Content-Type", "application/json")
w.Write(jsonMarshal)
}
Expand All @@ -136,37 +160,48 @@ func NowPlayingAlbumArt() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
queryResults, err := searchByTitleArtist(now.Song.Title, now.Song.Artist)
if err != nil {
clog.Error("NowPlayingAlbumArt", "Unable to search by title and artist.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
if len(queryResults) < 1 {
clog.Warn("NowPlayingAlbumArt", "The currently playing song could not be found in the database. The database may not be populated.")
w.WriteHeader(http.StatusNotFound) // 404 Not Found
return
}
path, err := getPathById(queryResults[0].ID)
if err != nil {
clog.Error("NowPlayingAlbumArt", "Unable to find file path by song ID.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
file, err := os.Open(path)
if err != nil {
clog.Error("NowPlayingAlbumArt", "Unable to open a file for album art extraction.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
tags, err := tag.ReadFrom(file)
if err != nil {
clog.Error("NowPlayingAlbumArt", "Unable to read tags on file for art extraction.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
if tags.Picture() == nil {
clog.Debug("NowPlayingAlbumArt", "The currently playing song has no album art metadata.")
w.WriteHeader(http.StatusNoContent) // 204 No Content
return
}
type SongData struct {
Picture []byte
}
result := SongData{Picture: tags.Picture().Data}
jsonMarshal, _ := json.Marshal(result)
jsonMarshal, err := json.Marshal(result)
if err != nil {
clog.Error("NowPlayingAlbumArt", "Failed to marshal art data.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(jsonMarshal)
}
Expand All @@ -176,7 +211,12 @@ func NowPlayingAlbumArt() http.HandlerFunc {
// Gets a list of the ten last-played songs, noting the time each ended.
func History() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
jsonMarshal, _ := json.Marshal(history)
jsonMarshal, err := json.Marshal(history)
if err != nil {
clog.Error("History", "Failed to marshal play history.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(jsonMarshal)
}
Expand All @@ -190,7 +230,12 @@ func ListenURL() http.HandlerFunc {
ListenURL string
}
listenurl := ListenURL{ListenURL: string(now.Host + "/" + now.Mountpoint)}
jsonMarshal, _ := json.Marshal(listenurl)
jsonMarshal, err := json.Marshal(listenurl)
if err != nil {
clog.Error("ListenURL", "Failed to marshal listen URL.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(jsonMarshal)
}
Expand All @@ -204,7 +249,12 @@ func Listeners() http.HandlerFunc {
Listeners int
}
listeners := Listeners{Listeners: int(now.Listeners)}
jsonMarshal, _ := json.Marshal(listeners)
jsonMarshal, err := json.Marshal(listeners)
if err != nil {
clog.Error("Listeners", "Failed to marshal listeners.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(jsonMarshal)
}
Expand All @@ -218,7 +268,12 @@ func Bitrate() http.HandlerFunc {
Bitrate int
}
bitrate := Bitrate{Bitrate: int(now.Bitrate)}
jsonMarshal, _ := json.Marshal(bitrate)
jsonMarshal, err := json.Marshal(bitrate)
if err != nil {
clog.Error("Bitrate", "Failed to marshal bitrate.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(jsonMarshal)
}
Expand All @@ -232,7 +287,12 @@ func Version() http.HandlerFunc {
Version string
}
version := Version{Version: c.Version}
jsonMarshal, _ := json.Marshal(version)
jsonMarshal, err := json.Marshal(version)
if err != nil {
clog.Error("Version", "Failed to marshal version.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(jsonMarshal)
}
Expand All @@ -253,6 +313,7 @@ func DevSkip() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_, err := liquidsoapSkip()
if err != nil {
clog.Error("DevSkip", "Unable to skip the playing song.", err)
w.WriteHeader(http.StatusInternalServerError) // 500 Internal Server Error
return
}
Expand Down
20 changes: 16 additions & 4 deletions cadence/server/api_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ func searchByQuery(query string) (queryResults []SongData, err error) {
}

// Takes a title and artist string to find a song which exactly matches.
// Returns a list of SongData whose one result is the first (best) match.
// Returns a list of SongData whose first result [0] is the first (best) match.
// This will not work if multiple songs share the exact same title and artist.
func searchByTitleArtist(title string, artist string) (queryResults []SongData, err error) {
title, artist = strings.TrimSpace(title), strings.TrimSpace(artist)
clog.Debug("searchByTitleArtist", fmt.Sprintf("Searching database for: '%s by %s", title, artist))
clog.Debug("searchByTitleArtist", fmt.Sprintf("Searching database for: %s by %s", title, artist))
selectStatement := fmt.Sprintf("SELECT id,artist,title,album,genre,year FROM %s WHERE title LIKE $1 AND artist LIKE $2;",
c.PostgresTableName)
rows, err := dbp.Query(selectStatement, title, artist)
Expand Down Expand Up @@ -121,7 +121,10 @@ func liquidsoapRequest(path string) (message string, err error) {
defer conn.Close()
// Push song request to source service, listen for a response, and quit the telnet session.
fmt.Fprintf(conn, "request.push "+path+"\n")
message, _ = bufio.NewReader(conn).ReadString('\n')
message, err = bufio.NewReader(conn).ReadString('\n')
if err != nil {
clog.Error("liquidsoapRequest", "Failed to read stream response message from audio source server.", err)
}
clog.Info("liquidsoapRequest", fmt.Sprintf("Message from audio source server: %s", message))
fmt.Fprintf(conn, "quit"+"\n")
return message, nil
Expand All @@ -137,7 +140,11 @@ func liquidsoapSkip() (message string, err error) {
defer conn.Close()
fmt.Fprintf(conn, "cadence1.skip\n")
// Listen for response
message, _ = bufio.NewReader(conn).ReadString('\n')
message, err = bufio.NewReader(conn).ReadString('\n')
if err != nil {
clog.Error("liquidsoapSkip", "Failed to read stream response message from audio source server.", err)
}
clog.Debug("liquidsoapSkip", fmt.Sprintf("Message from audio source server: %s", message))
fmt.Fprintf(conn, "quit"+"\n")
return message, nil
}
Expand Down Expand Up @@ -189,25 +196,30 @@ func icecastMonitor() {
if err != nil {
clog.Error("icecastMonitor", "Unable to stream data from the Icecast service.", err)
icecastDataReset()
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
clog.Debug("icecastMonitor", "Unable to connect to Icecast.")
icecastDataReset()
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
clog.Debug("icecastMonitor", "Connected to Icecast but unable to read response.")
icecastDataReset()
return
}
jsonParsed, err := gabs.ParseJSON([]byte(body))
if err != nil {
clog.Debug("icecastMonitor", "Connected to Icecast but unable to parse response.")
icecastDataReset()
return
}
if jsonParsed.Path("icestats.source.title").Data() == nil || jsonParsed.Path("icestats.source.artist").Data() == nil {
clog.Debug("icecastMonitor", "Connected to Icecast, but saw nothing playing.")
icecastDataReset()
return
}

now.Song.Artist = jsonParsed.Path("icestats.source.artist").Data().(string)
Expand Down
22 changes: 14 additions & 8 deletions cadence/server/db_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ func postgresInit() (err error) {
dbp, err = sql.Open("postgres", dsn)
if err != nil {
clog.Error("postgresInit", "Could not open connection to database.", err)
return err
}
err = dbp.Ping()
if err != nil {
clog.Error("postgresInit", "Could not successfully ping the metadata database.", err)
return err
}
// Enable fuzzystrmatch for levenshtein sorting.
// This enables the database to return results based on search similarity.
Expand Down Expand Up @@ -96,52 +98,56 @@ func postgresPopulate() error {
return err
}
}
clog.Debug("dbPopulate", "Verifying music metadata directory is accessible...")
clog.Debug("postgresPopulate", "Verifying music metadata directory is accessible...")
_, err = os.Stat(c.MusicDir)
if err != nil {
if os.IsNotExist(err) {
clog.Error("dbPopulate", "The configured target music directory was not found.", err)
clog.Error("postgresPopulate", "The configured target music directory was not found.", err)
return err
}
}

insertInto := fmt.Sprintf("INSERT INTO %s (%s, %s, %s, %s, %s, %s) SELECT $1, $2, $3, $4, $5, $6", c.PostgresTableName, "title", "album", "artist", "genre", "year", "path")
clog.Debug("dbPopulate", fmt.Sprintf("Extracting metadata from audio files in: <%s>", c.MusicDir))
clog.Debug("postgresPopulate", fmt.Sprintf("Extracting metadata from audio files in: <%s>", c.MusicDir))
err = filepath.Walk(c.MusicDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
clog.Error("postgresPopulate", "Error during filepath walk", err)
return err
}
clog.Debug("postgresPopulate", fmt.Sprintf("Populate analyzing file: <%s>", path))
if info.IsDir() {
clog.Debug("postgresPopulate", fmt.Sprintf("<%s> is a directory, skipping.", path))
return nil
}
extensions := []string{".mp3", ".flac", ".ogg"}
for _, ext := range extensions {
if strings.HasSuffix(path, ext) {
file, err := os.Open(path)
if err != nil {
clog.Error("dbPopulate", fmt.Sprintf("A problem occured opening <%s>.", path), err)
clog.Error("postgresPopulate", fmt.Sprintf("A problem occured opening <%s>.", path), err)
return err
}
defer file.Close()
tags, err := tag.ReadFrom(file)
if err != nil {
clog.Error("dbPopulate", fmt.Sprintf("A problem occured fetching tags from <%s>.", path), err)
clog.Error("postgresPopulate", fmt.Sprintf("A problem occured fetching tags from <%s>.", path), err)
return err
}
_, err = dbp.Exec(insertInto, tags.Title(), tags.Album(), tags.Artist(), tags.Genre(), tags.Year(), path)
if err != nil {
clog.Error("dbPopulate", fmt.Sprintf("A problem occured populating metadata for <%s>.", path), err)
clog.Error("postgresPopulate", fmt.Sprintf("A problem occured populating metadata for <%s>.", path), err)
return err
}
clog.Debug("postgresPopulate", fmt.Sprintf("Populated: %s by %s", tags.Title(), tags.Artist()))
break
}
}
return nil
})
if err != nil {
clog.Error("dbPopulate", "Music metadata database population failed, or may be incomplete.", err)
clog.Error("postgresPopulate", "Music metadata database population failed, or may be incomplete.", err)
return err
}
clog.Info("dbPopulate", "Database population completed.")
clog.Info("postgresPopulate", "Database population completed.")
return nil
}
Loading

0 comments on commit 241543e

Please sign in to comment.