A website inspired by Last.fm that will keep track of your listening statistics
lastfm music statistics
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add the same query functionality that was possible for artists to recordings. So searching on recordings and their scrobble count based on mbids, user id and datetime range.

oscar345 4c15334a beee691a

+232 -13
+9 -1
internal/filters/catalog.go
··· 6 6 "github.com/oscar345/keeptrack/pkg/pagination" 7 7 ) 8 8 9 - type Count struct { 9 + type ArtistCount struct { 10 10 Pagination pagination.Filter 11 11 From time.Time 12 12 To time.Time 13 13 UserID int 14 14 } 15 + 16 + type RecordingCount struct { 17 + Pagination pagination.Filter 18 + From time.Time 19 + To time.Time 20 + ArtistMBID string 21 + UserID int 22 + }
+10 -2
internal/models/catalog.go
··· 7 7 ImageURL string 8 8 } 9 9 10 + type ArtistCreditName struct { 11 + Artist Artist 12 + Name string 13 + JoinPhrase string 14 + Position int 15 + } 16 + 10 17 type Recording struct { 11 - MBID string 12 - Name string 18 + MBID string 19 + Name string 20 + Artists []ArtistCreditName 13 21 } 14 22 15 23 type Release struct {
+2
internal/models/scrobble.go
··· 1 1 package models 2 2 3 + // A generic struct that holds both the count and the mbid associated with that count. The mbid does 4 + // not have to be for a specific entity, and can be used for recordings, releases and artists, etc... 3 5 type CountMBID struct { 4 6 Count int 5 7 MBID string
+3 -3
internal/repo/db/artist_scrobble.go
··· 23 23 return &ArtistScrobbleRepoDB{duck: duck} 24 24 } 25 25 26 - func artistScrobbleCountWhere(filter filters.Count, wheres []string, args []any) ([]string, []any) { 26 + func artistScrobbleCountWhere(filter filters.ArtistCount, wheres []string, args []any) ([]string, []any) { 27 27 if filter.UserID != 0 { 28 28 wheres = append(wheres, "scrobble.user_id = ?") 29 29 args = append(args, filter.UserID) ··· 42 42 return wheres, args 43 43 } 44 44 45 - func (repo *ArtistScrobbleRepoDB) ListCount(ctx context.Context, filter filters.Count) ([]models.CountMBID, pagination.Page, error) { 45 + func (repo *ArtistScrobbleRepoDB) ListCount(ctx context.Context, filter filters.ArtistCount) ([]models.CountMBID, pagination.Page, error) { 46 46 var statement = /*sql*/ ` 47 47 WITH scrobbles AS ( 48 48 SELECT ··· 93 93 return items, pagination.New(filter.Pagination, total), nil 94 94 } 95 95 96 - func (repo *ArtistScrobbleRepoDB) GetCount(ctx context.Context, mbid string, filter filters.Count) (int, error) { 96 + func (repo *ArtistScrobbleRepoDB) GetCount(ctx context.Context, mbid string, filter filters.ArtistCount) (int, error) { 97 97 var statement = /*sql*/ ` 98 98 WITH scrobbles AS ( 99 99 SELECT
+35
internal/repo/db/models.go
··· 1 + package db 2 + 3 + import "github.com/oscar345/keeptrack/internal/models" 4 + 5 + // for some queries internal models for this packages are needed that should not be used outside of 6 + // this package, and models outside of this package should not be altered to be able to be used 7 + // inside this package 8 + 9 + type artist struct { 10 + MBID string `json:"mbid"` 11 + Name string `json:"name"` 12 + } 13 + 14 + func (a *artist) toModel() models.Artist { 15 + return models.Artist{ 16 + MBID: a.MBID, 17 + Name: a.Name, 18 + } 19 + } 20 + 21 + type artistCreditName struct { 22 + Artist artist `json:"artist"` 23 + Name string `json:"name"` 24 + JoinPhrase string `json:"join_phrase"` 25 + Position int `json:"position"` 26 + } 27 + 28 + func (a *artistCreditName) toModel() models.ArtistCreditName { 29 + return models.ArtistCreditName{ 30 + Artist: a.Artist.toModel(), 31 + Name: a.Name, 32 + JoinPhrase: a.JoinPhrase, 33 + Position: a.Position, 34 + } 35 + }
+61
internal/repo/db/recording.go
··· 3 3 import ( 4 4 "context" 5 5 "database/sql" 6 + "encoding/json" 7 + "fmt" 6 8 7 9 "github.com/oscar345/keeptrack/internal/models" 8 10 "github.com/oscar345/keeptrack/internal/repo" 9 11 "github.com/oscar345/keeptrack/pkg/database" 12 + "github.com/oscar345/keeptrack/pkg/enum" 10 13 ) 11 14 12 15 var _ repo.RecordingRepo = (*RecordingRepoDB)(nil) ··· 32 35 return model, nil 33 36 }) 34 37 } 38 + 39 + func (repo *RecordingRepoDB) ListByIDs(ctx context.Context, mbids []string) (map[string]models.Recording, error) { 40 + var statement = /*sql*/ ` 41 + SELECT 42 + recording.gid, 43 + recording.name, 44 + ( 45 + SELECT JSON_ARRAY( 46 + JSON_OBJECT( 47 + 'name', artist_credit_name.name, 48 + 'join_phrase', artist_credit_name.join_phrase, 49 + 'position', artist_credit_name.position, 50 + 'artist', JSON_OBJECT( 51 + 'mbid', artist.gid, 52 + 'name', artist.name 53 + ) 54 + ) 55 + ) 56 + FROM artist_credit 57 + INNER JOIN artist_credit_name ON artist_credit.id = artist_credit_name.artist_credit 58 + INNER JOIN artist ON artist_credit_name.artist = artist.id 59 + WHERE artist_credit.id = recording.artist_credit 60 + ) as artists 61 + FROM recording 62 + WHERE recording.gid IN (%s); 63 + ` 64 + statement = fmt.Sprintf(statement, database.GeneratePlaceholders(len(mbids))) 65 + 66 + args := []any{mbids} 67 + args = database.ExpandArgs(args) 68 + 69 + items, err := database.QueryMany(ctx, repo.db, statement, args, func(r *sql.Rows) (models.Recording, error) { 70 + var recording models.Recording 71 + var artistsJSON string 72 + 73 + if err := r.Scan(&recording.MBID, &recording.Name, &artistsJSON); err != nil { 74 + return models.Recording{}, err 75 + } 76 + 77 + var artists []artistCreditName 78 + if err := json.Unmarshal([]byte(artistsJSON), &artists); err != nil { 79 + return models.Recording{}, err 80 + } 81 + 82 + recording.Artists = enum.Map(artists, func(acn artistCreditName) models.ArtistCreditName { 83 + return acn.toModel() 84 + }) 85 + 86 + return recording, nil 87 + }) 88 + 89 + if err != nil { 90 + return nil, err 91 + } 92 + 93 + ordered := enum.OrderByKeys(items, mbids, func(item models.Recording) string { return item.MBID }) 94 + return enum.ZipMap(mbids, ordered), nil 95 + }
+99
internal/repo/db/recording_scrobble.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "fmt" 7 + "strings" 8 + 9 + "github.com/oscar345/keeptrack/internal/filters" 10 + "github.com/oscar345/keeptrack/internal/models" 11 + "github.com/oscar345/keeptrack/internal/repo" 12 + "github.com/oscar345/keeptrack/pkg/database" 13 + "github.com/oscar345/keeptrack/pkg/pagination" 14 + ) 15 + 16 + type RecordingScrobbleRepoDB struct { 17 + db *sql.DB 18 + } 19 + 20 + func NewRecordingScrobbleRepoDB(db *sql.DB) *RecordingScrobbleRepoDB { 21 + return &RecordingScrobbleRepoDB{db: db} 22 + } 23 + 24 + var _ repo.RecordingScrobbleRepo = (*RecordingScrobbleRepoDB)(nil) 25 + 26 + func recordingScrobbleCountWhere(filter filters.RecordingCount, wheres []string, args []any) ([]string, []any) { 27 + if filter.UserID != 0 { 28 + wheres = append(wheres, "scrobble.user_id = ?") 29 + args = append(args, filter.UserID) 30 + } 31 + 32 + if !filter.From.IsZero() { 33 + wheres = append(wheres, "scrobble.played_at >= ?") 34 + args = append(args, filter.From.UnixMicro()) 35 + } 36 + 37 + if !filter.To.IsZero() { 38 + wheres = append(wheres, "scrobble.played_at <= ?") 39 + args = append(args, filter.To.UnixMicro()) 40 + } 41 + 42 + return wheres, args 43 + } 44 + 45 + func (repo *RecordingScrobbleRepoDB) ListCount(ctx context.Context, filter filters.RecordingCount) ([]models.CountMBID, pagination.Page, error) { 46 + var statement = /*sql*/ ` 47 + WITH scrobbles AS ( 48 + SELECT 49 + scrobble.recording_mbid AS mbid, 50 + count(scrobble) AS amount 51 + FROM scrobble 52 + WHERE %s 53 + GROUP BY scrobble.recording_mbid 54 + ) 55 + SELECT 56 + scrobbles.mbid, 57 + scrobbles.amount, 58 + COUNT(*) OVER () AS total 59 + FROM scrobbles 60 + ORDER BY amount DESC 61 + OFFSET ? LIMIT ?; 62 + ` 63 + 64 + args := []any{} 65 + 66 + var total int 67 + database.QueryMany(ctx, repo.db, statement, args, func(r *sql.Rows) (models.CountMBID, error) { 68 + var model models.CountMBID 69 + if err := r.Scan(&model.MBID, &model.Count, &total); err != nil { 70 + return model, err 71 + } 72 + return model, nil 73 + }) 74 + 75 + return nil, pagination.Page{}, nil 76 + } 77 + 78 + func (repo *RecordingScrobbleRepoDB) GetCount(ctx context.Context, mbid string, filter filters.RecordingCount) (int, error) { 79 + var statement = /*sql*/ ` 80 + SELECT COUNT(*) FROM scrobbles 81 + WHERE %s 82 + ` 83 + args := []any{mbid} 84 + wheres := []string{"mbid = ?"} 85 + 86 + wheres, args = recordingScrobbleCountWhere(filter, wheres, args) 87 + 88 + if len(wheres) > 0 { 89 + statement = fmt.Sprintf(statement, strings.Join(wheres, " AND ")) 90 + } 91 + 92 + return database.QueryOne(ctx, repo.db, statement, args, func(r *sql.Rows) (int, error) { 93 + var count int 94 + if err := r.Scan(&count); err != nil { 95 + return 0, err 96 + } 97 + return count, nil 98 + }) 99 + }
+8 -2
internal/repo/repo.go
··· 14 14 } 15 15 16 16 type ArtistScrobbleRepo interface { 17 - ListCount(ctx context.Context, filter filters.Count) ([]models.CountMBID, pagination.Page, error) 18 - GetCount(ctx context.Context, mbid string, filter filters.Count) (int, error) 17 + ListCount(ctx context.Context, filter filters.ArtistCount) ([]models.CountMBID, pagination.Page, error) 18 + GetCount(ctx context.Context, mbid string, filter filters.ArtistCount) (int, error) 19 19 } 20 20 21 21 type ArtistFollowRepo interface { ··· 25 25 } 26 26 27 27 type RecordingRepo interface { 28 + ListByIDs(ctx context.Context, mbids []string) (map[string]models.Recording, error) 28 29 GetByID(ctx context.Context, mbid string) (models.Recording, error) 30 + } 31 + 32 + type RecordingScrobbleRepo interface { 33 + ListCount(ctx context.Context, filter filters.RecordingCount) ([]models.CountMBID, pagination.Page, error) 34 + GetCount(ctx context.Context, mbid string, filter filters.RecordingCount) (int, error) 29 35 } 30 36 31 37 type RecordingLikeRepo interface {
+1 -1
internal/services/artist.go
··· 37 37 } 38 38 } 39 39 40 - func (as *ArtistService) ListArtistByCount(ctx context.Context, filter filters.Count) ([]models.Artist, pagination.Page, error) { 40 + func (as *ArtistService) ListArtistByCount(ctx context.Context, filter filters.ArtistCount) ([]models.Artist, pagination.Page, error) { 41 41 // Since this should return a general list, we do not want to filter on user. The user service 42 42 // should be used to get the user specific list. 43 43 filter.UserID = 0
+1 -1
internal/services/user.go
··· 27 27 } 28 28 29 29 func (us *UserService) ListArtistsByCount( 30 - ctx context.Context, userID int, filter filters.Count, 30 + ctx context.Context, userID int, filter filters.ArtistCount, 31 31 ) ([]models.Artist, pagination.Page, error) { 32 32 filter.UserID = userID 33 33 counts, page, err := us.artistScrobbleRepo.ListCount(ctx, filter)
+2 -2
internal/web/requests/catalog.go
··· 30 30 return nil 31 31 } 32 32 33 - func (req Count) Filter() (filters.Count, error) { 34 - var filter filters.Count 33 + func (req Count) Filter() (filters.ArtistCount, error) { 34 + var filter filters.ArtistCount 35 35 36 36 if err := ParseTime(req.From, &filter.From); err != nil { 37 37 return filter, err
+1 -1
internal/web/router/router.go
··· 79 79 }) 80 80 81 81 r.Get("/library/artists", func(w http.ResponseWriter, r *http.Request) { 82 - artists, page, err := s.artistService.ListArtistByCount(r.Context(), filters.Count{ 82 + artists, page, err := s.artistService.ListArtistByCount(r.Context(), filters.ArtistCount{ 83 83 UserID: 1, 84 84 }) 85 85