Skip to content

Commit

Permalink
Lidarr support and cleanup. (#11)
Browse files Browse the repository at this point in the history
Lidarr support, lint and bug fixes. FreeBSD package fixed.
  • Loading branch information
davidnewhall authored Jan 4, 2021
1 parent 4c01de5 commit 87d0fa6
Show file tree
Hide file tree
Showing 14 changed files with 412 additions and 106 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/*.conf
/*.gz
/*.zip
/*.upx
/unpackerr*.1
/*.deb
/*.rpm
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ dmg: clean macapp

# Delete all build assets.
clean:
rm -f $(BINARY) $(BINARY).*.{macos,freebsd,linux,exe}{,.gz,.zip} $(BINARY).1{,.gz} $(BINARY).rb
rm -f $(BINARY) $(BINARY).*.{macos,freebsd,linux,exe,upx}{,.gz,.zip} $(BINARY).1{,.gz} $(BINARY).rb
rm -f $(BINARY){_,-}*.{deb,rpm,txz} v*.tar.gz.sha256 examples/MANUAL .metadata.make
rm -f cmd/$(BINARY)/README{,.html} README{,.html} ./$(BINARY)_manual.html rsrc.syso $(MACAPP).app.zip
rm -rf package_build_* release after-install-rendered.sh before-remove-rendered.sh $(MACAPP).app
Expand Down Expand Up @@ -297,7 +297,7 @@ package_build_linux_armhf: package_build_linux armhf
cp $(BINARY).armhf.linux $@/usr/bin/$(BINARY)

# Build an environment that can be packaged for freebsd.
package_build_freebsd: readme man freebsd
package_build_freebsd: readme man after-install-rendered.sh before-remove-rendered.sh freebsd
mkdir -p $@/usr/local/bin $@/usr/local/etc/$(BINARY) $@/usr/local/share/man/man1 $@/usr/local/share/doc/$(BINARY)
cp $(BINARY).amd64.freebsd $@/usr/local/bin/$(BINARY)
cp *.1.gz $@/usr/local/share/man/man1
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module github.com/Go-Lift-TV/discordnotifier-client

go 1.15


require (
github.com/gen2brain/dlgs v0.0.0-20201118155338-03fe7f81ad25
github.com/getlantern/golog v0.0.0-20201105130739-9586b8bde3a9 // indirect
Expand All @@ -15,6 +16,6 @@ require (
golang.org/x/sys v0.0.0-20201223074533-0d417f636930 // indirect
golift.io/cnfg v0.0.7
golift.io/rotatorr v0.0.0-20201213130124-94efc0b9aff1
golift.io/starr v0.9.5-0.20201225031430-d96b4216b1b8
golift.io/starr v0.9.5-0.20210104053210-306f1822c914
golift.io/version v0.0.2
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ golift.io/rotatorr v0.0.0-20201213130124-94efc0b9aff1 h1:8SDkFT5QpXyN24BCPw5Yux7
golift.io/rotatorr v0.0.0-20201213130124-94efc0b9aff1/go.mod h1:EZevRvIGRh8jDMwuYL0/tlPns0KynquPZzb0SerIC1s=
golift.io/starr v0.9.5-0.20201225031430-d96b4216b1b8 h1:3FMaGhdPSsP8G1D3jD4mCMyVnD/DPTAuNkSZVRxNzSw=
golift.io/starr v0.9.5-0.20201225031430-d96b4216b1b8/go.mod h1:EE8B7OlqZlE/EGmBP1bLsK1OHEgWwbNpyjDXX0B2f0Y=
golift.io/starr v0.9.5-0.20201230044237-9b54fd5a1c2c h1:/knsNcSIiH4eZftBXRM5uYuA3Wki51ePWOsGxf26wM4=
golift.io/starr v0.9.5-0.20201230044237-9b54fd5a1c2c/go.mod h1:EE8B7OlqZlE/EGmBP1bLsK1OHEgWwbNpyjDXX0B2f0Y=
golift.io/starr v0.9.5-0.20210102172243-614fc1de1548 h1:10xWqUGL/yotNrEaJpquZ1ppvgZN1lkxFqs++f2Xvnk=
golift.io/starr v0.9.5-0.20210102172243-614fc1de1548/go.mod h1:EE8B7OlqZlE/EGmBP1bLsK1OHEgWwbNpyjDXX0B2f0Y=
golift.io/starr v0.9.5-0.20210104053210-306f1822c914 h1:gG2bli4+C6dCkl4Y/z2DUm4V7Ihwmd5pJn9CGA5V5G4=
golift.io/starr v0.9.5-0.20210104053210-306f1822c914/go.mod h1:EE8B7OlqZlE/EGmBP1bLsK1OHEgWwbNpyjDXX0B2f0Y=
golift.io/version v0.0.2 h1:i0gXRuSDHKs4O0sVDUg4+vNIuOxYoXhaxspftu2FRTE=
golift.io/version v0.0.2/go.mod h1:76aHNz8/Pm7CbuxIsDi97jABL5Zui3f2uZxDm4vB6hU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
2 changes: 1 addition & 1 deletion init/bsd/freebsd.rc.d
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ name="{{BINARYU}}"
real_name="{{BINARY}}"
rcvar="{{BINARYU}}_enable"
{{BINARYU}}_command="/usr/local/bin/${real_name}"
{{BINARYU}}_user="nobody"
{{BINARYU}}_user="{{BINARYU}}"
{{BINARYU}}_config="/usr/local/etc/${real_name}/{{CONFIG_FILE}}"
pidfile="/var/run/${real_name}/pid"

Expand Down
31 changes: 20 additions & 11 deletions pkg/apps/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ var (
ErrNoLidarr = fmt.Errorf("configured lidarr ID not found")
ErrNoReadarr = fmt.Errorf("configured readarr ID not found")
ErrExists = fmt.Errorf("the requested item already exists")
ErrNotFound = fmt.Errorf("the request returned an empty payload")
)

// APIHandler is our custom handler function for APIs.
Expand Down Expand Up @@ -94,7 +95,7 @@ func (a *Apps) HandleAPIpath(app App, api string, next APIHandler, method ...str
a.Respond(w, http.StatusUnprocessableEntity, fmt.Errorf("%v: %w", id, ErrNoReadarr), start)

// These store the application configuration (starr) in a context then pass that into the next method.
// They retrieve the return code and output, then send a response (a.respond).
// They retrieve the return code and output, then send a response (a.Respond).
case app == Radarr:
i, m := next(r.WithContext(context.WithValue(r.Context(), Radarr, a.Radarr[id-1])))
a.Respond(w, i, m, start)
Expand All @@ -116,9 +117,10 @@ func (a *Apps) checkAPIKey(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-API-Key") != a.APIKey {
w.WriteHeader(http.StatusUnauthorized)
} else {
next.ServeHTTP(w, r)
return
}

next.ServeHTTP(w, r)
})
}

Expand All @@ -133,22 +135,23 @@ func (a *Apps) InitHandlers() {
// Setup creates request interfaces and sets the timeout for each server.
func (a *Apps) Setup(timeout time.Duration) {
for i := range a.Radarr {
a.Radarr[i].fix(timeout)
a.Radarr[i].setup(timeout)
}

for i := range a.Readarr {
a.Readarr[i].fix(timeout)
a.Readarr[i].setup(timeout)
}

for i := range a.Sonarr {
a.Sonarr[i].fix(timeout)
a.Sonarr[i].setup(timeout)
}

for i := range a.Lidarr {
a.Lidarr[i].fix(timeout)
a.Lidarr[i].setup(timeout)
}
}

// Respond sends a standard response to our caller. JSON encoded blobs.
func (a *Apps) Respond(w http.ResponseWriter, stat int, msg interface{}, start time.Time) {
w.Header().Set("X-Request-Time", time.Since(start).Round(time.Microsecond).String())
w.Header().Set("Content-Type", "application/json")
Expand All @@ -157,13 +160,19 @@ func (a *Apps) Respond(w http.ResponseWriter, stat int, msg interface{}, start t
statusTxt := strconv.Itoa(stat) + ": " + http.StatusText(stat)

if m, ok := msg.(error); ok {
a.ErrorLog.Printf("Status: %s, Message: %v", statusTxt, m)
a.ErrorLog.Printf("Request failed. Status: %s, Message: %v", statusTxt, m)
msg = m.Error()
}

b, _ := json.Marshal(map[string]interface{}{"status": statusTxt, "message": msg})
_, _ = w.Write(b)
_, _ = w.Write([]byte("\n")) // curl likes new lines.
b, err := json.Marshal(map[string]interface{}{"status": statusTxt, "message": msg})
if err != nil {
a.ErrorLog.Printf("JSON marshal failed. Status: %s, Error: %v, Message: %v", statusTxt, err, msg)
}

size, err := w.Write(append(b, '\n')) // curl likes new lines.
if err != nil {
a.ErrorLog.Printf("Response failed. Written: %d/%d, Status: %s, Error: %v", size, len(b)+1, statusTxt, err)
}
}

/* Every API call runs one of these methods to find the interface for the respective app. */
Expand Down
165 changes: 148 additions & 17 deletions pkg/apps/lidarr.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/gorilla/mux"
Expand All @@ -18,10 +20,16 @@ mbid - music brainz is the source for lidarr (todo)
// lidarrHandlers is called once on startup to register the web API paths.
func (a *Apps) lidarrHandlers() {
a.HandleAPIpath(Lidarr, "/add", lidarrAddAlbum, "POST")
a.HandleAPIpath(Lidarr, "/check/{albumid:[-a-z0-9]+}", lidarrCheckAlbum, "GET")
a.HandleAPIpath(Lidarr, "/check/{mbid:[-a-z0-9]+}", lidarrCheckAlbum, "GET")
a.HandleAPIpath(Lidarr, "/search/{query}", lidarrSearchAlbum, "GET")
a.HandleAPIpath(Lidarr, "/get/{albumid:[0-9]+}", lidarrGetAlbum, "GET")
a.HandleAPIpath(Lidarr, "/update", lidarrUpdateAlbum, "PUT")
a.HandleAPIpath(Lidarr, "/qualityProfiles", lidarrProfiles, "GET")
a.HandleAPIpath(Lidarr, "/qualityDefinitions", lidarrQualityDefs, "GET")
a.HandleAPIpath(Lidarr, "/metadataProfiles", lidarrMetadata, "GET")
a.HandleAPIpath(Lidarr, "/rootFolder", lidarrRootFolders, "GET")
a.HandleAPIpath(Lidarr, "/artist/{artistid:[0-9]+}", lidarrGetArtist, "GET")
a.HandleAPIpath(Lidarr, "/updateartist", lidarrUpdateArtist, "PUT")
}

// LidarrConfig represents the input data for a Lidarr server.
Expand All @@ -30,7 +38,7 @@ type LidarrConfig struct {
lidarr *lidarr.Lidarr
}

func (r *LidarrConfig) fix(timeout time.Duration) {
func (r *LidarrConfig) setup(timeout time.Duration) {
r.lidarr = lidarr.New(r.Config)
if r.Timeout.Duration == 0 {
r.Timeout.Duration = timeout
Expand Down Expand Up @@ -69,6 +77,21 @@ func lidarrProfiles(r *http.Request) (int, interface{}) {
return http.StatusOK, p
}

func lidarrMetadata(r *http.Request) (int, interface{}) {
profiles, err := getLidarr(r).GetMetadataProfiles()
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("getting profiles: %w", err)
}

// Format profile ID=>Name into a nice map.
p := make(map[int64]string)
for i := range profiles {
p[profiles[i].ID] = profiles[i].Name
}

return http.StatusOK, p
}

func lidarrQualityDefs(r *http.Request) (int, interface{}) {
// Get the profiles from lidarr.
definitions, err := getLidarr(r).GetQualityDefinition()
Expand All @@ -86,40 +109,148 @@ func lidarrQualityDefs(r *http.Request) (int, interface{}) {
}

func lidarrCheckAlbum(r *http.Request) (int, interface{}) {
// Check for existing movie.
if m, err := getLidarr(r).GetAlbum(mux.Vars(r)["albumid"]); err != nil {
id := mux.Vars(r)["mbid"]

m, err := getLidarr(r).GetAlbum(id)
if err != nil {
return http.StatusServiceUnavailable, fmt.Errorf("checking album: %w", err)
} else if len(m) > 0 {
return http.StatusConflict, fmt.Errorf("%s: %w", mux.Vars(r)["albumid"], ErrExists)
return http.StatusConflict, fmt.Errorf("%s: %w", id, ErrExists)
}

return http.StatusOK, http.StatusText(http.StatusNotFound)
}

func lidarrGetAlbum(r *http.Request) (int, interface{}) {
albumID, _ := strconv.ParseInt(mux.Vars(r)["albumid"], 10, 64)

album, err := getLidarr(r).GetAlbumByID(albumID)
if err != nil {
return http.StatusServiceUnavailable, fmt.Errorf("checking album: %w", err)
}

return http.StatusOK, album
}

func lidarrGetArtist(r *http.Request) (int, interface{}) {
artistID, _ := strconv.ParseInt(mux.Vars(r)["artistid"], 10, 64)

artist, err := getLidarr(r).GetArtistByID(artistID)
if err != nil {
return http.StatusServiceUnavailable, fmt.Errorf("checking artist: %w", err)
}

return http.StatusOK, artist
}

func lidarrUpdateAlbum(r *http.Request) (int, interface{}) {
var album lidarr.Album

err := json.NewDecoder(r.Body).Decode(&album)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("decoding payload: %w", err)
}

_, err = getLidarr(r).UpdateAlbum(album.ID, &album)
if err != nil {
return http.StatusServiceUnavailable, fmt.Errorf("updating album: %w", err)
}

return http.StatusOK, "success"
}

func lidarrUpdateArtist(r *http.Request) (int, interface{}) {
var artist lidarr.Artist

err := json.NewDecoder(r.Body).Decode(&artist)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("decoding payload: %w", err)
}

_, err = getLidarr(r).UpdateArtist(&artist)
if err != nil {
return http.StatusServiceUnavailable, fmt.Errorf("updating artist: %w", err)
}

return http.StatusOK, "success"
}

func lidarrAddAlbum(r *http.Request) (int, interface{}) {
var payload lidarr.AddAlbumInput
// Extract payload and check for TMDB ID.
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {

err := json.NewDecoder(r.Body).Decode(&payload)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("decoding payload: %w", err)
} else if payload == nil {
return http.StatusUnprocessableEntity, fmt.Errorf("0: %w", ErrNoGRID)
} else if payload.ForeignAlbumID == "" {
return http.StatusUnprocessableEntity, fmt.Errorf("0: %w", ErrNoMBID)
}

lidar := getLidarr(r)
app := getLidarr(r)
// Check for existing album.
/* broken:
if m, err := lidar.GetAlbum(payload.AlbumID); err != nil {
m, err := app.GetAlbum(payload.ForeignAlbumID)
if err != nil {
return http.StatusServiceUnavailable, fmt.Errorf("checking album: %w", err)
} else if len(m) > 0 {
return http.StatusConflict, fmt.Errorf("%d: %w", payload.AlbumID, ErrExists)
return http.StatusConflict, fmt.Errorf("%s: %w", payload.ForeignAlbumID, ErrExists)
}
*/

// Add book using payload.
book, err := lidar.AddAlbum(&payload)
album, err := app.AddAlbum(&payload)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("adding album: %w", err)
}

return http.StatusCreated, book
return http.StatusCreated, album
}

func lidarrSearchAlbum(r *http.Request) (int, interface{}) {
albums, err := getLidarr(r).GetAlbum("")
if err != nil {
return http.StatusServiceUnavailable, fmt.Errorf("getting albums: %w", err)
}

query := strings.TrimSpace(strings.ToLower(mux.Vars(r)["query"])) // in
output := make([]map[string]interface{}, 0) // out

for _, album := range albums {
if albumSearch(query, album.Title, album.Releases) {
a := map[string]interface{}{
"id": album.ID,
"mbid": album.ForeignAlbumID,
"metadataId": album.Artist.MetadataProfileID,
"qualityId": album.Artist.QualityProfileID,
"title": album.Title,
"release": album.ReleaseDate,
"artistId": album.ArtistID,
"artistName": album.Artist.ArtistName,
"profileId": album.ProfileID,
"overview": album.Overview,
"ratings": album.Ratings.Value,
"type": album.AlbumType,
"exists": false,
"files": 0,
}

if album.Statistics != nil {
a["exists"] = album.Statistics.SizeOnDisk > 0
}

output = append(output, a)
}
}

return http.StatusOK, output
}

func albumSearch(query, title string, releases []*lidarr.Release) bool {
if strings.Contains(strings.ToLower(title), query) {
return true
}

for _, t := range releases {
if strings.Contains(strings.ToLower(t.Title), query) {
return true
}
}

return false
}
Loading

0 comments on commit 87d0fa6

Please sign in to comment.