diff --git a/server/ctrlsubsonic/handlers_by_tags.go b/server/ctrlsubsonic/handlers_by_tags.go index f99302b..e0c2343 100644 --- a/server/ctrlsubsonic/handlers_by_tags.go +++ b/server/ctrlsubsonic/handlers_by_tags.go @@ -9,6 +9,7 @@ import ( "go.senan.xyz/gonic/server/ctrlsubsonic/params" "go.senan.xyz/gonic/server/ctrlsubsonic/spec" + "go.senan.xyz/gonic/server/ctrlsubsonic/specid" "go.senan.xyz/gonic/server/db" "go.senan.xyz/gonic/server/lastfm" ) @@ -263,9 +264,11 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response { if gorm.IsRecordNotFoundError(err) && !inclNotPresent { continue } - similar := &spec.SimilarArtist{ID: -1} + similar := &spec.SimilarArtist{ + ID: specid.ID{Type: specid.Artist, Value: -1}, + } if artist.ID != 0 { - similar.ID = artist.ID + similar.ID = artist.SID() } similar.Name = similarInfo.Name similar.AlbumCount = artist.AlbumCount diff --git a/server/ctrlsubsonic/params/params.go b/server/ctrlsubsonic/params/params.go index ffe9816..229ced2 100644 --- a/server/ctrlsubsonic/params/params.go +++ b/server/ctrlsubsonic/params/params.go @@ -30,15 +30,15 @@ import ( "net/url" "strconv" - "go.senan.xyz/gonic/server/ids" + "go.senan.xyz/gonic/server/ctrlsubsonic/specid" ) // some thin wrappers // may be needed when cleaning up parse() below -func parseStr(in string) (string, error) { return in, nil } -func parseInt(in string) (int, error) { return strconv.Atoi(in) } -func parseID(in string) (ids.IDV, error) { return ids.Parse(in) } -func parseBool(in string) (bool, error) { return strconv.ParseBool(in) } +func parseStr(in string) (string, error) { return in, nil } +func parseInt(in string) (int, error) { return strconv.Atoi(in) } +func parseID(in string) (specid.ID, error) { return specid.New(in) } +func parseBool(in string) (bool, error) { return strconv.ParseBool(in) } func parse(values []string, i interface{}) error { if len(values) == 0 { @@ -50,7 +50,7 @@ func parse(values []string, i interface{}) error { *v, err = parseStr(values[0]) case *int: *v, err = parseInt(values[0]) - case *ids.IDV: + case *specid.ID: *v, err = parseID(values[0]) case *bool: *v, err = parseBool(values[0]) @@ -70,7 +70,7 @@ func parse(values []string, i interface{}) error { } *v = append(*v, parsed) } - case *[]ids.IDV: + case *[]specid.ID: for _, value := range values { parsed, err := parseID(value) if err != nil { @@ -229,56 +229,56 @@ func (p Params) GetFirstOrIntList(or []int, keys ...string) []int { return or } -// ** begin ids.IDV {get, get first, get or, get first or} +// ** begin specid.ID {get, get first, get or, get first or} -func (p Params) GetID(key string) (ids.IDV, error) { - var ret ids.IDV +func (p Params) GetID(key string) (specid.ID, error) { + var ret specid.ID return ret, parse(p.get(key), &ret) } -func (p Params) GetFirstID(keys ...string) (ids.IDV, error) { - var ret ids.IDV +func (p Params) GetFirstID(keys ...string) (specid.ID, error) { + var ret specid.ID return ret, parse(p.getFirst(keys), &ret) } -func (p Params) GetOrID(key string, or ids.IDV) ids.IDV { - var ret ids.IDV +func (p Params) GetOrID(key string, or specid.ID) specid.ID { + var ret specid.ID if err := parse(p.get(key), &ret); err == nil { return ret } return or } -func (p Params) GetFirstOrID(or ids.IDV, keys ...string) ids.IDV { - var ret ids.IDV +func (p Params) GetFirstOrID(or specid.ID, keys ...string) specid.ID { + var ret specid.ID if err := parse(p.getFirst(keys), &ret); err == nil { return ret } return or } -// ** begin []ids.IDV {get, get first, get or, get first or} +// ** begin []specid.ID {get, get first, get or, get first or} -func (p Params) GetIDList(key string) ([]ids.IDV, error) { - var ret []ids.IDV +func (p Params) GetIDList(key string) ([]specid.ID, error) { + var ret []specid.ID return ret, parse(p.get(key), &ret) } -func (p Params) GetFirstIDList(keys ...string) ([]ids.IDV, error) { - var ret []ids.IDV +func (p Params) GetFirstIDList(keys ...string) ([]specid.ID, error) { + var ret []specid.ID return ret, parse(p.getFirst(keys), &ret) } -func (p Params) GetOrIDList(key string, or []ids.IDV) []ids.IDV { - var ret []ids.IDV +func (p Params) GetOrIDList(key string, or []specid.ID) []specid.ID { + var ret []specid.ID if err := parse(p.get(key), &ret); err == nil { return ret } return or } -func (p Params) GetFirstOrIDList(or []ids.IDV, keys ...string) []ids.IDV { - var ret []ids.IDV +func (p Params) GetFirstOrIDList(or []specid.ID, keys ...string) []specid.ID { + var ret []specid.ID if err := parse(p.getFirst(keys), &ret); err == nil { return ret } diff --git a/server/ctrlsubsonic/spec/construct_by_folder.go b/server/ctrlsubsonic/spec/construct_by_folder.go index c439be3..b6d851e 100644 --- a/server/ctrlsubsonic/spec/construct_by_folder.go +++ b/server/ctrlsubsonic/spec/construct_by_folder.go @@ -3,42 +3,41 @@ package spec import ( "path" - "go.senan.xyz/gonic/server/ctrlsubsonic/params" "go.senan.xyz/gonic/server/db" ) func NewAlbumByFolder(f *db.Album) *Album { a := &Album{ Artist: f.Parent.RightPath, - ID: params.IDAlbum(f.ID), + ID: f.SID(), IsDir: true, - ParentID: params.IDAlbum(f.ParentID), + ParentID: f.ParentSID(), Title: f.RightPath, TrackCount: f.ChildCount, } if f.Cover != "" { - a.CoverID = f.ID + a.CoverID = f.SID() } return a } func NewTCAlbumByFolder(f *db.Album) *TrackChild { trCh := &TrackChild{ - ID: params.IDAlbum(f.ID), + ID: f.SID(), IsDir: true, Title: f.RightPath, - ParentID: params.IDAlbum(f.ParentID), + ParentID: f.ParentSID(), CreatedAt: f.UpdatedAt, } if f.Cover != "" { - trCh.CoverID = f.ID + trCh.CoverID = f.SID() } return trCh } func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild { trCh := &TrackChild{ - ID: params.IDTrack(t.ID), + ID: t.SID(), ContentType: t.MIME(), Suffix: t.Ext(), Size: t.Size, @@ -51,7 +50,7 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild { parent.RightPath, t.Filename, ), - ParentID: params.IDAlbum(parent.ID), + ParentID: parent.SID(), Duration: t.Length, Bitrate: t.Bitrate, IsDir: false, @@ -59,7 +58,7 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild { CreatedAt: t.CreatedAt, } if parent.Cover != "" { - trCh.CoverID = parent.ID + trCh.CoverID = parent.SID() } if t.Album != nil { trCh.Album = t.Album.RightPath @@ -73,7 +72,7 @@ func NewArtistByFolder(f *db.Album) *Artist { // from an "album" where // maybe TODO: rename the Album model to Folder return &Artist{ - ID: params.IDAlbum(f.ID), + ID: f.SID(), Name: f.RightPath, AlbumCount: f.ChildCount, } @@ -81,13 +80,13 @@ func NewArtistByFolder(f *db.Album) *Artist { func NewDirectoryByFolder(f *db.Album, children []*TrackChild) *Directory { dir := &Directory{ - ID: f.ID, + ID: f.SID(), Name: f.RightPath, Children: children, } // don't show the root dir as a parent if f.ParentID != 1 { - dir.ParentID = f.ParentID + dir.ParentID = f.ParentSID() } return dir } diff --git a/server/ctrlsubsonic/spec/construct_by_tags.go b/server/ctrlsubsonic/spec/construct_by_tags.go index 310200a..16b99c1 100644 --- a/server/ctrlsubsonic/spec/construct_by_tags.go +++ b/server/ctrlsubsonic/spec/construct_by_tags.go @@ -9,7 +9,7 @@ import ( func NewAlbumByTags(a *db.Album, artist *db.Artist) *Album { ret := &Album{ Created: a.ModifiedAt, - ID: a.ID, + ID: a.SID(), Name: a.TagTitle, Year: a.TagYear, TrackCount: a.ChildCount, @@ -18,21 +18,21 @@ func NewAlbumByTags(a *db.Album, artist *db.Artist) *Album { ret.Genre = a.TagGenre.Name } if a.Cover != "" { - ret.CoverID = a.ID + ret.CoverID = a.SID() } if artist != nil { ret.Artist = artist.Name - ret.ArtistID = artist.ID + ret.ArtistID = artist.SID() } return ret } func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild { ret := &TrackChild{ - ID: t.ID, + ID: t.SID(), ContentType: t.MIME(), Suffix: t.Ext(), - ParentID: t.AlbumID, + ParentID: t.AlbumSID(), CreatedAt: t.CreatedAt, Size: t.Size, Title: t.TagTitle, @@ -45,16 +45,16 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild { t.Filename, ), Album: album.TagTitle, - AlbumID: album.ID, + AlbumID: album.SID(), Duration: t.Length, Bitrate: t.Bitrate, Type: "music", } if album.Cover != "" { - ret.CoverID = album.ID + ret.CoverID = album.SID() } if album.TagArtist != nil { - ret.ArtistID = album.TagArtist.ID + ret.ArtistID = album.TagArtist.SID() } // replace tags that we're present if ret.Title == "" { @@ -71,7 +71,7 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild { func NewArtistByTags(a *db.Artist) *Artist { return &Artist{ - ID: a.ID, + ID: a.SID(), Name: a.Name, AlbumCount: a.AlbumCount, } diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go index bb2359d..eb23f49 100644 --- a/server/ctrlsubsonic/spec/spec.go +++ b/server/ctrlsubsonic/spec/spec.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "go.senan.xyz/gonic/server/ctrlsubsonic/specid" "go.senan.xyz/gonic/version" ) @@ -90,15 +91,15 @@ type Albums struct { type Album struct { // common - ID string `xml:"id,attr,omitempty" json:"id"` - CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty,string"` - ArtistID string `xml:"artistId,attr,omitempty" json:"artistId,omitempty"` - Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"` + ID specid.ID `xml:"id,attr,omitempty" json:"id"` + CoverID specid.ID `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"` + ArtistID specid.ID `xml:"artistId,attr,omitempty" json:"artistId,omitempty"` + Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"` // browsing by folder (eg. getAlbumList) - Title string `xml:"title,attr" json:"title"` - Album string `xml:"album,attr" json:"album"` - ParentID string `xml:"parent,attr,omitempty" json:"parent,omitempty"` - IsDir bool `xml:"isDir,attr,omitempty" json:"isDir,omitempty"` + Title string `xml:"title,attr" json:"title"` + Album string `xml:"album,attr" json:"album"` + ParentID specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"` + IsDir bool `xml:"isDir,attr,omitempty" json:"isDir,omitempty"` // browsing by tags (eg. getAlbumList2) Name string `xml:"name,attr" json:"name"` TrackCount int `xml:"songCount,attr" json:"songCount"` @@ -119,19 +120,19 @@ type TracksByGenre struct { type TrackChild struct { Album string `xml:"album,attr,omitempty" json:"album,omitempty"` - AlbumID string `xml:"albumId,attr,omitempty" json:"albumId,omitempty"` + AlbumID specid.ID `xml:"albumId,attr,omitempty" json:"albumId,omitempty"` Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"` - ArtistID string `xml:"artistId,attr,omitempty" json:"artistId,omitempty"` + ArtistID specid.ID `xml:"artistId,attr,omitempty" json:"artistId,omitempty"` Bitrate int `xml:"bitRate,attr,omitempty" json:"bitRate,omitempty"` ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"` - CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty,string"` + CoverID specid.ID `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"` CreatedAt time.Time `xml:"created,attr,omitempty" json:"created,omitempty"` Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"` Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"` - ID string `xml:"id,attr,omitempty" json:"id,omitempty"` + ID specid.ID `xml:"id,attr,omitempty" json:"id,omitempty"` IsDir bool `xml:"isDir,attr" json:"isDir"` IsVideo bool `xml:"isVideo,attr" json:"isVideo"` - ParentID string `xml:"parent,attr,omitempty" json:"parent,omitempty"` + ParentID specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"` Path string `xml:"path,attr,omitempty" json:"path,omitempty"` Size int `xml:"size,attr,omitempty" json:"size,omitempty"` Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"` @@ -147,11 +148,11 @@ type Artists struct { } type Artist struct { - ID string `xml:"id,attr,omitempty" json:"id"` - Name string `xml:"name,attr" json:"name"` - CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty,string"` - AlbumCount int `xml:"albumCount,attr" json:"albumCount"` - Albums []*Album `xml:"album,omitempty" json:"album,omitempty"` + ID specid.ID `xml:"id,attr,omitempty" json:"id"` + Name string `xml:"name,attr" json:"name"` + CoverID specid.ID `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"` + AlbumCount int `xml:"albumCount,attr" json:"albumCount"` + Albums []*Album `xml:"album,omitempty" json:"album,omitempty"` } type Indexes struct { @@ -166,8 +167,8 @@ type Index struct { } type Directory struct { - ID string `xml:"id,attr,omitempty" json:"id"` - ParentID string `xml:"parent,attr,omitempty" json:"parent,omitempty"` + ID specid.ID `xml:"id,attr,omitempty" json:"id"` + ParentID specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"` Name string `xml:"name,attr,omitempty" json:"name"` Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"` Children []*TrackChild `xml:"child,omitempty" json:"child,omitempty"` @@ -178,7 +179,7 @@ type MusicFolders struct { } type MusicFolder struct { - ID string `xml:"id,attr,omitempty" json:"id,omitempty"` + ID int `xml:"id,attr,omitempty" json:"id,omitempty"` Name string `xml:"name,attr,omitempty" json:"name,omitempty"` } @@ -238,9 +239,9 @@ type Playlist struct { } type SimilarArtist struct { - ID string `xml:"id,attr" json:"id"` - Name string `xml:"name,attr" json:"name"` - AlbumCount int `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"` + ID specid.ID `xml:"id,attr" json:"id"` + Name string `xml:"name,attr" json:"name"` + AlbumCount int `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"` } type ArtistInfo struct { diff --git a/server/ctrlsubsonic/specid/ids.go b/server/ctrlsubsonic/specid/ids.go new file mode 100644 index 0000000..5912682 --- /dev/null +++ b/server/ctrlsubsonic/specid/ids.go @@ -0,0 +1,59 @@ +package specid + +// this package is at such a high level in the hierarchy because +// it's used by both `server/db` (for now) and `server/ctrlsubsonic` + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" +) + +var ( + ErrBadSeparator = errors.New("bad separator") + ErrNotAnInt = errors.New("not an int") + ErrBadPrefix = errors.New("bad prefix") +) + +type IDT string + +const ( + Artist IDT = "ar" + Album IDT = "al" + Track IDT = "tr" + separator = "-" +) + +type ID struct { + Type IDT + Value int +} + +func New(in string) (ID, error) { + parts := strings.Split(in, separator) + if len(parts) != 2 { + return ID{}, ErrBadSeparator + } + partType := parts[0] + partValue := parts[1] + val, err := strconv.Atoi(partValue) + if err != nil { + return ID{}, fmt.Errorf("%q: %w", partValue, ErrNotAnInt) + } + for _, acc := range []IDT{Artist, Album, Track} { + if partType == string(acc) { + return ID{Type: acc, Value: val}, nil + } + } + return ID{}, fmt.Errorf("%q: %w", partType, ErrBadPrefix) +} + +func (i ID) String() string { + return fmt.Sprintf("%s%s%d", i.Type, separator, i.Value) +} + +func (i ID) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} diff --git a/server/ids/ids_test.go b/server/ctrlsubsonic/specid/ids_test.go similarity index 86% rename from server/ids/ids_test.go rename to server/ctrlsubsonic/specid/ids_test.go index 6129c49..c2c826d 100644 --- a/server/ids/ids_test.go +++ b/server/ctrlsubsonic/specid/ids_test.go @@ -1,14 +1,14 @@ -package ids +package specid import ( "errors" "testing" ) -func TestParse(t *testing.T) { +func TestParseID(t *testing.T) { tcases := []struct { param string - expType ID + expType IDT expValue int expErr error }{ @@ -19,8 +19,9 @@ func TestParse(t *testing.T) { {param: "al-howdy", expErr: ErrNotAnInt}, } for _, tcase := range tcases { + tcase := tcase // pin t.Run(tcase.param, func(t *testing.T) { - act, err := Parse(tcase.param) + act, err := New(tcase.param) if !errors.Is(err, tcase.expErr) { t.Fatalf("expected err %q, got %q", tcase.expErr, err) } diff --git a/server/db/model.go b/server/db/model.go index a66877c..37ecf06 100644 --- a/server/db/model.go +++ b/server/db/model.go @@ -11,6 +11,9 @@ import ( "strings" "time" + // TODO: remove this dep + + "go.senan.xyz/gonic/server/ctrlsubsonic/specid" "go.senan.xyz/gonic/server/mime" ) @@ -46,6 +49,10 @@ type Artist struct { AlbumCount int `sql:"-"` } +func (a *Artist) SID() specid.ID { + return specid.ID{Type: specid.Artist, Value: a.ID} +} + func (a *Artist) IndexName() string { if len(a.NameUDec) > 0 { return a.NameUDec @@ -85,6 +92,18 @@ type Track struct { TagBrainzID string `sql:"default: null"` } +func (t *Track) SID() specid.ID { + return specid.ID{Type: specid.Track, Value: t.ID} +} + +func (t *Track) AlbumSID() specid.ID { + return specid.ID{Type: specid.Album, Value: t.AlbumID} +} + +func (t *Track) ArtistSID() specid.ID { + return specid.ID{Type: specid.Artist, Value: t.ArtistID} +} + func (t *Track) Ext() string { longExt := path.Ext(t.Filename) if len(longExt) < 1 { @@ -157,6 +176,14 @@ type Album struct { ReceivedTags bool `gorm:"-"` } +func (a *Album) SID() specid.ID { + return specid.ID{Type: specid.Album, Value: a.ID} +} + +func (a *Album) ParentSID() specid.ID { + return specid.ID{Type: specid.Album, Value: a.ParentID} +} + func (a *Album) IndexRightPath() string { if len(a.RightPathUDec) > 0 { return a.RightPathUDec diff --git a/server/ids/ids.go b/server/ids/ids.go deleted file mode 100644 index 1acd91d..0000000 --- a/server/ids/ids.go +++ /dev/null @@ -1,59 +0,0 @@ -package ids - -// this package is at such a high level in the hierarchy because -// it's used by both `server/db` (for now) and `server/ctrlsubsonic` - -import ( - "errors" - "fmt" - "strconv" - "strings" -) - -var ( - ErrBadSeparator = errors.New("bad separator") - ErrNotAnInt = errors.New("not an int") - ErrBadPrefix = errors.New("bad prefix") -) - -type ID string - -const ( - // type values copied from subsonic - Artist ID = "ar" - Album ID = "al" - Track ID = "tr" -) - -var accepted = []ID{Artist, - Album, - Track, -} - -type IDV struct { - Type ID - Value int -} - -func (i IDV) String() string { - return fmt.Sprintf("%s-%d", i.Type, i.Value) -} - -func Parse(in string) (IDV, error) { - parts := strings.Split(in, "-") - if len(parts) != 2 { - return IDV{}, ErrBadSeparator - } - partType := parts[0] - partValue := parts[1] - val, err := strconv.Atoi(partValue) - if err != nil { - return IDV{}, fmt.Errorf("%q: %w", partValue, ErrNotAnInt) - } - for _, acc := range accepted { - if partType == string(acc) { - return IDV{Type: acc, Value: val}, nil - } - } - return IDV{}, fmt.Errorf("%q: %w", partType, ErrBadPrefix) -}