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.

Make changes to the services to divide the resources it depends on into king (for the artist service there is now a user repos struct to split the repos from the other resources). Also change the filters to use a daterange struct.

oscar345 15d6ce5e 8564840d

+228 -344
+8 -6
internal/filters/catalog.go
··· 6 6 "github.com/oscar345/keeptrack/pkg/pagination" 7 7 ) 8 8 9 + type DateRange struct { 10 + From time.Time 11 + To time.Time 12 + } 13 + 9 14 type ArtistCount struct { 10 15 Pagination pagination.Filter 11 - From time.Time 12 - To time.Time 16 + Date DateRange 13 17 UserID int 14 18 } 15 19 16 20 type RecordingCount struct { 17 21 Pagination pagination.Filter 18 - From time.Time 19 - To time.Time 22 + Date DateRange 20 23 ArtistMBID string 21 24 ReleaseMBID string 22 25 UserID int ··· 24 27 25 28 type ReleaseCount struct { 26 29 Pagination pagination.Filter 27 - From time.Time 28 - To time.Time 30 + Date DateRange 29 31 ArtistMBID string 30 32 UserID int 31 33 }
+5 -4
internal/models/catalog.go
··· 22 22 } 23 23 24 24 type Release struct { 25 - MBID string 26 - Name string 27 - Count int 28 - Artists []ArtistCreditName 25 + MBID string 26 + Name string 27 + Count int 28 + Artists []ArtistCreditName 29 + ImageURL string 29 30 }
-148
internal/providers/image.go
··· 1 - package providers 2 - 3 - import ( 4 - "context" 5 - "errors" 6 - "log" 7 - "path" 8 - "sync" 9 - 10 - "github.com/oscar345/keeptrack/internal/image" 11 - storagesvc "github.com/oscar345/keeptrack/pkg/storage" 12 - "golang.org/x/sync/errgroup" 13 - ) 14 - 15 - type MediaProvider struct { 16 - storage storagesvc.Storage 17 - artistImageFetcher image.ArtistImageFetcher 18 - releaseImageFetcher image.ReleaseImageFetcher 19 - } 20 - 21 - func NewMediaProvider( 22 - storage storagesvc.Storage, 23 - artistImageFetcher image.ArtistImageFetcher, 24 - releaseImageFetcher image.ReleaseImageFetcher, 25 - ) *MediaProvider { 26 - return &MediaProvider{ 27 - storage: storage, 28 - artistImageFetcher: artistImageFetcher, 29 - releaseImageFetcher: releaseImageFetcher, 30 - } 31 - } 32 - 33 - func (mp *MediaProvider) GetArtistImage(ctx context.Context, mbid string) (string, error) { 34 - filename := createArtistImageFilename(mbid) 35 - 36 - if mp.storage.Exists(filename) { 37 - return mp.storage.GetURL(filename), nil 38 - } 39 - 40 - images, err := mp.artistImageFetcher.ListImages(ctx, mbid) 41 - if err != nil { 42 - return "", err 43 - } 44 - 45 - if len(images) == 0 { 46 - return "", errors.New("no images found") 47 - } 48 - image := images[0] 49 - 50 - content, err := mp.artistImageFetcher.FetchImage(ctx, image) 51 - if err != nil { 52 - return "", err 53 - } 54 - 55 - if err := mp.storage.Save(content, filename); err != nil { 56 - return "", err 57 - } 58 - 59 - return mp.storage.GetURL(filename), nil 60 - } 61 - 62 - func (mp *MediaProvider) ListArtistsImages(ctx context.Context, mbids []string) (map[string]string, error) { 63 - g, ctx := errgroup.WithContext(ctx) 64 - 65 - var mu sync.Mutex 66 - result := make(map[string]string, len(mbids)) 67 - 68 - for _, mbid := range mbids { 69 - g.Go(func() error { 70 - if url, err := mp.GetArtistImage(ctx, mbid); err == nil { 71 - mu.Lock() 72 - result[mbid] = url 73 - mu.Unlock() 74 - } else { 75 - log.Printf("failed to get artist image for %s: %v", mbid, err) 76 - } 77 - return nil 78 - }) 79 - } 80 - 81 - if err := g.Wait(); err != nil { 82 - return nil, err 83 - } 84 - 85 - return result, nil 86 - } 87 - 88 - func createArtistImageFilename(mbid string) string { 89 - return path.Join("images", "artists", mbid) 90 - } 91 - 92 - func createReleaseImageFilename(mbid string) string { 93 - return path.Join("images", "releases", mbid) 94 - } 95 - 96 - func (mp *MediaProvider) GetReleaseImage(ctx context.Context, mbid string) (string, error) { 97 - filename := createReleaseImageFilename(mbid) 98 - if mp.storage.Exists(filename) { 99 - return mp.storage.GetURL(filename), nil 100 - } 101 - 102 - images, err := mp.releaseImageFetcher.ListImages(ctx, mbid) 103 - if err != nil { 104 - return "", err 105 - } 106 - 107 - if len(images) == 0 { 108 - return "", errors.New("no images found") 109 - } 110 - 111 - image := images[0] 112 - 113 - content, err := mp.releaseImageFetcher.FetchImage(ctx, image) 114 - if err != nil { 115 - return "", err 116 - } 117 - 118 - if err := mp.storage.Save(content, filename); err != nil { 119 - return "", err 120 - } 121 - 122 - return mp.storage.GetURL(filename), nil 123 - } 124 - 125 - func (mp *MediaProvider) ListReleasesImages(ctx context.Context, mbids []string) (map[string]string, error) { 126 - g, ctx := errgroup.WithContext(ctx) 127 - 128 - var mu sync.Mutex 129 - result := make(map[string]string, len(mbids)) 130 - 131 - for _, mbid := range mbids { 132 - g.Go(func() error { 133 - if image, err := mp.GetReleaseImage(ctx, mbid); err != nil { 134 - mu.Lock() 135 - result[mbid] = image 136 - mu.Unlock() 137 - } 138 - 139 - return nil 140 - }) 141 - } 142 - 143 - if err := g.Wait(); err != nil { 144 - return nil, err 145 - } 146 - 147 - return result, nil 148 - }
+4 -23
internal/repo/db/artist_scrobble.go
··· 22 22 return &ArtistScrobbleRepoDB{duck: duck} 23 23 } 24 24 25 - func artistScrobbleCountWhere(filter filters.ArtistCount, wheres []string, args []any) ([]string, []any) { 26 - if filter.UserID != 0 { 27 - wheres = append(wheres, "scrobble.user_id = ?") 28 - args = append(args, filter.UserID) 29 - } 30 - 31 - if !filter.From.IsZero() { 32 - wheres = append(wheres, "scrobble.played_at >= ?") 33 - args = append(args, filter.From) 34 - } 35 - 36 - if !filter.To.IsZero() { 37 - wheres = append(wheres, "scrobble.played_at <= ?") 38 - args = append(args, filter.To) 39 - } 40 - 41 - return wheres, args 42 - } 43 - 44 25 func (repo *ArtistScrobbleRepoDB) ListCount(ctx context.Context, filter filters.ArtistCount) ([]models.CountMBID, pagination.Page, error) { 45 26 var statement = /*sql*/ ` 46 27 WITH scrobbles AS ( ··· 65 46 66 47 wheres, args := database.Where(make([]string, 0), make([]any, 0), 67 48 database.Condition{SQL: "scrobble.user_id = ?", Value: filter.UserID, Ok: filter.UserID != 0}, 68 - database.Condition{SQL: "scrobble.played_at >= ?", Value: filter.From, Ok: !filter.From.IsZero()}, 69 - database.Condition{SQL: "scrobble.played_at <= ?", Value: filter.To, Ok: !filter.To.IsZero()}, 49 + database.Condition{SQL: "scrobble.played_at >= ?", Value: filter.Date.From, Ok: !filter.Date.From.IsZero()}, 50 + database.Condition{SQL: "scrobble.played_at <= ?", Value: filter.Date.To, Ok: !filter.Date.To.IsZero()}, 70 51 ) 71 52 72 53 statement = fmt.Sprintf(statement, database.WhereSQL(wheres)) ··· 106 87 args := []any{mbid} 107 88 wheres, args := database.Where(make([]string, 0), args, 108 89 database.Condition{SQL: "scrobble.user_id = ?", Value: filter.UserID, Ok: filter.UserID != 0}, 109 - database.Condition{SQL: "scrobble.played_at >= ?", Value: filter.From, Ok: !filter.From.IsZero()}, 110 - database.Condition{SQL: "scrobble.played_at <= ?", Value: filter.To, Ok: !filter.To.IsZero()}, 90 + database.Condition{SQL: "scrobble.played_at >= ?", Value: filter.Date.From, Ok: !filter.Date.From.IsZero()}, 91 + database.Condition{SQL: "scrobble.played_at <= ?", Value: filter.Date.To, Ok: !filter.Date.To.IsZero()}, 111 92 ) 112 93 113 94 statement = fmt.Sprintf(statement, database.WhereSQL(wheres))
+6 -6
internal/repo/db/recording_scrobble.go
··· 41 41 SELECT 42 42 scrobble.* 43 43 FROM scrobble 44 - INNER JOIN recording_mbid__release_mbid AS rr ON rr.recording_mbid = scrobble.recording_mbid 45 - WHERE rr.release_mbid = ? 44 + INNER JOIN recording_mbid__release_group_mbid AS rrg ON rrg.recording_mbid = scrobble.recording_mbid 45 + WHERE rrg.release_group_mbid = ? 46 46 )` 47 47 args = append(args, filter.ReleaseMBID) 48 48 default: ··· 69 69 ` 70 70 71 71 wheres, args := database.Where(make([]string, 0), args, 72 - database.Condition{SQL: "scrobbles.played_at <= ?", Ok: !filter.To.IsZero(), Value: filter.To}, 73 - database.Condition{SQL: "scrobbles.played_at >= ?", Ok: !filter.From.IsZero(), Value: filter.From}, 72 + database.Condition{SQL: "scrobbles.played_at <= ?", Ok: !filter.Date.To.IsZero(), Value: filter.Date.To}, 73 + database.Condition{SQL: "scrobbles.played_at >= ?", Ok: !filter.Date.From.IsZero(), Value: filter.Date.From}, 74 74 database.Condition{SQL: "scrobbles.user_id = ?", Ok: filter.UserID != 0, Value: filter.UserID}, 75 75 ) 76 76 args = append(args, filter.Pagination.Limit(), filter.Pagination.Offset()) ··· 102 102 // length is always one since Ok is always true for mbid condition 103 103 wheres, args := database.Where(make([]string, 0), make([]any, 0), 104 104 database.Condition{SQL: "scrobble.mbid = ?", Ok: true, Value: mbid}, 105 - database.Condition{SQL: "scrobble.played_at <= ?", Ok: !filter.To.IsZero(), Value: filter.To}, 106 - database.Condition{SQL: "scrobble.played_at >= ?", Ok: !filter.From.IsZero(), Value: filter.From}, 105 + database.Condition{SQL: "scrobble.played_at <= ?", Ok: !filter.Date.To.IsZero(), Value: filter.Date.To}, 106 + database.Condition{SQL: "scrobble.played_at >= ?", Ok: !filter.Date.From.IsZero(), Value: filter.Date.From}, 107 107 database.Condition{SQL: "scrobble.user_id = ?", Ok: filter.UserID != 0, Value: filter.UserID}, 108 108 ) 109 109
+10 -3
internal/repo/db/release.go
··· 69 69 70 70 func (repo *ReleaseRepoDB) GetByID(ctx context.Context, mbid string) (models.Release, error) { 71 71 var statement = /*sql*/ ` 72 - SELECT release_group.name, release_group.mbid, (%s) as artists 72 + SELECT release_group.name, release_group.gid, (%s) as artists 73 73 FROM release_group 74 - WHERE release_group.mbid = ?; 74 + WHERE release_group.gid = ?; 75 75 ` 76 76 statement = fmt.Sprintf(statement, artistCreditNamesJsonStatement("release_group.artist_credit")) 77 77 78 78 return database.QueryOne(ctx, repo.db, statement, []any{mbid}, func(r *sql.Rows) (models.Release, error) { 79 79 var model models.Release 80 - if err := r.Scan(&model.Name, &model.MBID); err != nil { 80 + var artistJSON string 81 + 82 + if err := r.Scan(&model.Name, &model.MBID, &artistJSON); err != nil { 81 83 return model, err 82 84 } 85 + 86 + if err := json.Unmarshal([]byte(artistJSON), &model.Artists); err != nil { 87 + return model, err 88 + } 89 + 83 90 return model, nil 84 91 }) 85 92 }
+6 -6
internal/repo/db/release_scrobble.go
··· 63 63 ` 64 64 65 65 wheres, args := database.Where(make([]string, 0), args, 66 - database.Condition{SQL: "scrobbles.played_at <= ?", Ok: !filter.To.IsZero(), Value: filter.To}, 67 - database.Condition{SQL: "scrobbles.played_at >= ?", Ok: !filter.From.IsZero(), Value: filter.From}, 66 + database.Condition{SQL: "scrobbles.played_at <= ?", Ok: !filter.Date.To.IsZero(), Value: filter.Date.To}, 67 + database.Condition{SQL: "scrobbles.played_at >= ?", Ok: !filter.Date.From.IsZero(), Value: filter.Date.From}, 68 68 database.Condition{SQL: "scrobbles.user_id = ?", Ok: filter.UserID != 0, Value: filter.UserID}, 69 69 ) 70 70 args = append(args, filter.Pagination.Offset(), filter.Pagination.Limit()) ··· 97 97 FROM scrobble 98 98 INNER JOIN recording_mbid__release_group_mbid as rrg 99 99 ON rrg.recording_mbid = scrobble.recording_mbid 100 - WHERE rrg.artist_mbid = ? AND %s 101 - GROUP BY rrg.artist_mbid 100 + WHERE rrg.release_group_mbid = ? AND %s 101 + GROUP BY rrg.release_group_mbid 102 102 ) 103 103 SELECT COALESCE(sum(scrobbles.amount), 0) AS amount 104 104 FROM scrobbles ··· 107 107 args := []any{mbid} 108 108 wheres, args := database.Where(make([]string, 0), args, 109 109 database.Condition{SQL: "scrobble.user_id = ?", Value: filter.UserID, Ok: filter.UserID != 0}, 110 - database.Condition{SQL: "scrobble.played_at >= ?", Value: filter.From, Ok: !filter.From.IsZero()}, 111 - database.Condition{SQL: "scrobble.played_at <= ?", Value: filter.To, Ok: !filter.To.IsZero()}, 110 + database.Condition{SQL: "scrobble.played_at >= ?", Value: filter.Date.From, Ok: !filter.Date.From.IsZero()}, 111 + database.Condition{SQL: "scrobble.played_at <= ?", Value: filter.Date.To, Ok: !filter.Date.To.IsZero()}, 112 112 ) 113 113 114 114 statement = fmt.Sprintf(statement, database.WhereSQL(wheres))
+4 -5
internal/server/server.go
··· 8 8 _ "github.com/duckdb/duckdb-go/v2" 9 9 "github.com/oscar345/keeptrack/internal/config" 10 10 "github.com/oscar345/keeptrack/internal/image" 11 - "github.com/oscar345/keeptrack/internal/providers" 12 11 "github.com/oscar345/keeptrack/internal/repo/db" 13 12 "github.com/oscar345/keeptrack/internal/services" 14 13 "github.com/oscar345/keeptrack/internal/web/router" ··· 84 83 85 84 storage := storagesvc.NewDiskStorage("/public", s.config.Storage.Disk.Path) 86 85 87 - mediaProvider := providers.NewMediaProvider(storage, artistImageFetcher, releaseImageFetcher) 86 + imageService := services.NewImageService(storage, artistImageFetcher, releaseImageFetcher) 88 87 89 88 return Services{ 90 89 Artist: services.NewArtistService( 91 - artistRepo, artistScrobbleRepo, artistImageFetcher, mediaProvider, storage, 90 + artistRepo, artistScrobbleRepo, *imageService, storage, 92 91 ), 93 92 User: services.NewUserService( 94 93 userRepo, ··· 96 95 userServiceRepo, 97 96 artistRepo, 98 97 artistScrobbleRepo, 99 - mediaProvider, 98 + imageService, 100 99 ), 101 100 Recording: services.NewRecordingService(recordingRepo, recordingLikeRepo, recordingScrobbleRepo), 102 - Release: services.NewReleaseService(releaseRepo, releaseScrobbleRepo), 101 + Release: services.NewReleaseService(releaseRepo, releaseScrobbleRepo, *imageService), 103 102 } 104 103 }
+26 -20
internal/services/artist.go
··· 4 4 "context" 5 5 6 6 "github.com/oscar345/keeptrack/internal/filters" 7 - "github.com/oscar345/keeptrack/internal/image" 8 7 "github.com/oscar345/keeptrack/internal/models" 9 - "github.com/oscar345/keeptrack/internal/providers" 10 8 "github.com/oscar345/keeptrack/internal/repo" 11 9 "github.com/oscar345/keeptrack/pkg/enum" 12 10 "github.com/oscar345/keeptrack/pkg/pagination" 13 11 storagesvc "github.com/oscar345/keeptrack/pkg/storage" 14 12 ) 15 13 14 + type artistRepos struct { 15 + artist repo.ArtistRepo 16 + scrobble repo.ArtistScrobbleRepo 17 + } 18 + type artistDependencies struct { 19 + image ImageService 20 + } 21 + 16 22 type ArtistService struct { 17 - artistRepo repo.ArtistRepo 18 - artistScrobbleRepo repo.ArtistScrobbleRepo 19 - artistImageFetcher image.ArtistImageFetcher 20 - mediaProvider *providers.MediaProvider 21 - storage storagesvc.Storage 23 + repos artistRepos 24 + dependencies artistDependencies 25 + storage storagesvc.Storage 22 26 } 23 27 24 28 func NewArtistService( 25 29 artistRepo repo.ArtistRepo, 26 30 artistScrobbleRepo repo.ArtistScrobbleRepo, 27 - artistImageFetcher image.ArtistImageFetcher, 28 - mediaProvider *providers.MediaProvider, 31 + ImageService ImageService, 29 32 storage storagesvc.Storage, 30 33 ) ArtistService { 31 34 return ArtistService{ 32 - artistRepo: artistRepo, 33 - artistScrobbleRepo: artistScrobbleRepo, 34 - artistImageFetcher: artistImageFetcher, 35 - mediaProvider: mediaProvider, 36 - storage: storage, 35 + repos: artistRepos{ 36 + artist: artistRepo, 37 + scrobble: artistScrobbleRepo, 38 + }, 39 + dependencies: artistDependencies{ 40 + image: ImageService, 41 + }, 42 + storage: storage, 37 43 } 38 44 } 39 45 40 46 func (as *ArtistService) GetByID(ctx context.Context, mbid string) (models.Artist, error) { 41 47 // if there is no image because of an error, just ignore the error for now 42 - image, _ := as.mediaProvider.GetArtistImage(ctx, mbid) 48 + image, _ := as.dependencies.image.GetArtistImage(ctx, mbid) 43 49 44 - artist, err := as.artistRepo.GetByID(ctx, mbid) 50 + artist, err := as.repos.artist.GetByID(ctx, mbid) 45 51 if err != nil { 46 52 return models.Artist{}, err 47 53 } ··· 52 58 } 53 59 54 60 func (as *ArtistService) GetCountByID(ctx context.Context, mbid string, filter filters.ArtistCount) (int, error) { 55 - return as.artistScrobbleRepo.GetCount(ctx, mbid, filter) 61 + return as.repos.scrobble.GetCount(ctx, mbid, filter) 56 62 } 57 63 58 64 func (as *ArtistService) ListByCount(ctx context.Context, filter filters.ArtistCount) ([]models.Artist, pagination.Page, error) { ··· 60 66 // should be used to get the user specific list. 61 67 filter.UserID = 0 62 68 63 - counts, page, err := as.artistScrobbleRepo.ListCount(ctx, filter) 69 + counts, page, err := as.repos.scrobble.ListCount(ctx, filter) 64 70 if err != nil { 65 71 return nil, page, err 66 72 } ··· 69 75 return item.MBID 70 76 }) 71 77 72 - artists, err := as.artistRepo.ListByIDs(ctx, mbids) 78 + artists, err := as.repos.artist.ListByIDs(ctx, mbids) 73 79 if err != nil { 74 80 return nil, page, err 75 81 } 76 82 77 - images, err := as.mediaProvider.ListArtistsImages(ctx, mbids) 83 + images, err := as.dependencies.image.ListArtistsImages(ctx, mbids) 78 84 79 85 if err != nil { 80 86 return nil, page, err
+46 -9
internal/services/release.go
··· 11 11 "github.com/oscar345/keeptrack/pkg/pagination" 12 12 ) 13 13 14 - type ReleaseRepos struct { 14 + type releaseRepos struct { 15 15 release repo.ReleaseRepo 16 16 scrobble repo.ReleaseScrobbleRepo 17 17 } 18 18 19 + type releaseDependencies struct { 20 + image ImageService 21 + } 22 + 19 23 type ReleaseService struct { 20 - repos ReleaseRepos 24 + repos releaseRepos 25 + dependencies releaseDependencies 21 26 } 22 27 23 - func NewReleaseService(release repo.ReleaseRepo, scrobble repo.ReleaseScrobbleRepo) ReleaseService { 28 + func NewReleaseService(release repo.ReleaseRepo, scrobble repo.ReleaseScrobbleRepo, imageService ImageService) ReleaseService { 24 29 return ReleaseService{ 25 - repos: ReleaseRepos{ 30 + repos: releaseRepos{ 26 31 release: release, 27 32 scrobble: scrobble, 28 33 }, 34 + dependencies: releaseDependencies{ 35 + image: imageService, 36 + }, 29 37 } 30 38 } 31 39 ··· 37 45 return nil, page, err 38 46 } 39 47 40 - fmt.Println("counts from 1", counts) 41 - releases, err := rs.repos.release.ListByIDs(ctx, enum.Map(counts, func(item models.CountMBID) string { 42 - return item.MBID 43 - })) 44 - fmt.Println("releases from 1", releases) 48 + mbids := enum.Map(counts, func(item models.CountMBID) string { return item.MBID }) 49 + 50 + releases, err := rs.repos.release.ListByIDs(ctx, mbids) 51 + if err != nil { 52 + return nil, page, err 53 + } 54 + 55 + images, err := rs.dependencies.image.ListReleasesImages(ctx, mbids) 45 56 if err != nil { 46 57 return nil, page, err 47 58 } 48 59 60 + fmt.Println("images from 1", images) 61 + 49 62 result := enum.Map(counts, func(item models.CountMBID) models.Release { 50 63 release := releases[item.MBID] 64 + release.ImageURL = images[item.MBID] 51 65 release.Count = item.Count 52 66 return release 53 67 }) 54 68 55 69 return result, page, err 56 70 } 71 + 72 + func (rs *ReleaseService) GetCountByID(ctx context.Context, mbid string, filter filters.ReleaseCount) (int, error) { 73 + return rs.repos.scrobble.GetCount(ctx, mbid, filter) 74 + } 75 + 76 + func (rs *ReleaseService) GetByID(ctx context.Context, mbid string) (models.Release, error) { 77 + // if there is no image because of an error, just ignore the error for now 78 + image, err := rs.dependencies.image.GetReleaseImage(ctx, mbid) 79 + if err != nil { 80 + image = "" 81 + } 82 + 83 + release, err := rs.repos.release.GetByID(ctx, mbid) 84 + if err != nil { 85 + return models.Release{}, err 86 + } 87 + 88 + release.ImageURL = image 89 + 90 + fmt.Println(release) 91 + 92 + return release, nil 93 + }
+28 -58
internal/services/user.go
··· 3 3 import ( 4 4 "context" 5 5 6 - "github.com/oscar345/keeptrack/internal/filters" 7 6 "github.com/oscar345/keeptrack/internal/models" 8 - "github.com/oscar345/keeptrack/internal/providers" 9 7 "github.com/oscar345/keeptrack/internal/repo" 10 - "github.com/oscar345/keeptrack/pkg/enum" 11 8 "github.com/oscar345/keeptrack/pkg/pagination" 12 9 "golang.org/x/oauth2" 13 10 ) 14 11 12 + type userDependencies struct { 13 + image *ImageService 14 + } 15 + 16 + type userRepos struct { 17 + user repo.UserRepo 18 + follow repo.UserFollowRepo 19 + service repo.UserServiceRepo 20 + artist repo.ArtistRepo 21 + artistScrobble repo.ArtistScrobbleRepo 22 + } 23 + 15 24 type UserService struct { 16 - userRepo repo.UserRepo 17 - userFollowRepo repo.UserFollowRepo 18 - userServiceRepo repo.UserServiceRepo 19 - artistRepo repo.ArtistRepo 20 - artistScrobbleRepo repo.ArtistScrobbleRepo 21 - mediaProvider *providers.MediaProvider 25 + repos userRepos 26 + dependencies userDependencies 22 27 } 23 28 24 29 func NewUserService( ··· 27 32 userServiceRepo repo.UserServiceRepo, 28 33 artistRepo repo.ArtistRepo, 29 34 artistScrobbleRepo repo.ArtistScrobbleRepo, 30 - mediaProvider *providers.MediaProvider, 35 + imageService *ImageService, 31 36 ) UserService { 32 37 return UserService{ 33 - userRepo: userRepo, 34 - userFollowRepo: userFollowRepo, 35 - userServiceRepo: userServiceRepo, 36 - artistRepo: artistRepo, 37 - artistScrobbleRepo: artistScrobbleRepo, 38 - mediaProvider: mediaProvider, 39 - } 40 - } 41 - 42 - func (us *UserService) ListArtistsByCount( 43 - ctx context.Context, userID int, filter filters.ArtistCount, 44 - ) ([]models.Artist, pagination.Page, error) { 45 - filter.UserID = userID 46 - counts, page, err := us.artistScrobbleRepo.ListCount(ctx, filter) 47 - if err != nil { 48 - return nil, page, err 49 - } 50 - 51 - mbids := enum.Map(counts, func(item models.CountMBID) string { 52 - return item.MBID 53 - }) 54 - 55 - artists, err := us.artistRepo.ListByIDs(ctx, mbids) 56 - if err != nil { 57 - return nil, page, err 38 + repos: userRepos{ 39 + user: userRepo, 40 + follow: userFollowRepo, 41 + service: userServiceRepo, 42 + artistScrobble: artistScrobbleRepo, 43 + artist: artistRepo, 44 + }, 45 + dependencies: userDependencies{ 46 + image: imageService, 47 + }, 58 48 } 59 - 60 - images, err := us.mediaProvider.ListArtistsImages(ctx, mbids) 61 - 62 - if err != nil { 63 - return nil, page, err 64 - } 65 - 66 - items := enum.Map(counts, func(item models.CountMBID) models.Artist { 67 - artist := artists[item.MBID] 68 - image := images[item.MBID] 69 - 70 - return models.Artist{ 71 - MBID: item.MBID, 72 - Name: artist.Name, 73 - Count: item.Count, 74 - ImageURL: image, 75 - } 76 - }) 77 - 78 - return items, page, nil 79 49 } 80 50 81 51 func (us *UserService) ListFollowers( 82 52 ctx context.Context, userID int, filter pagination.Filter, 83 53 ) ([]models.User, pagination.Page, error) { 84 - return us.userFollowRepo.ListFollowers(ctx, userID, filter) 54 + return us.repos.follow.ListFollowers(ctx, userID, filter) 85 55 } 86 56 87 57 func (us *UserService) ListFollowing( 88 58 ctx context.Context, userID int, filter pagination.Filter, 89 59 ) ([]models.User, pagination.Page, error) { 90 - return us.userFollowRepo.ListFollowing(ctx, userID, filter) 60 + return us.repos.follow.ListFollowing(ctx, userID, filter) 91 61 } 92 62 93 63 func (us *UserService) CreateUserService(ctx context.Context, userID int, service string, token *oauth2.Token) error { 94 - _, err := us.userServiceRepo.Create(ctx, models.UserService{ 64 + _, err := us.repos.service.Create(ctx, models.UserService{ 95 65 UserID: userID, 96 66 Service: service, 97 67 Token: token.AccessToken,
+35 -29
internal/web/requests/catalog.go
··· 7 7 "github.com/oscar345/keeptrack/pkg/pagination" 8 8 ) 9 9 10 + type DateRange struct { 11 + From time.Time `json:"from"` 12 + To time.Time `json:"to"` 13 + } 14 + 15 + func (dr DateRange) Filter() filters.DateRange { 16 + return filters.DateRange{ 17 + From: dr.From, 18 + To: dr.To, 19 + } 20 + } 21 + 10 22 type Pagination struct { 11 23 Page int `json:"page"` 12 24 Size int `json:"size"` 13 25 } 14 26 15 - type Count struct { 27 + func (pr Pagination) Filter() pagination.Filter { 28 + return pagination.Filter{ 29 + Page: pr.Page, 30 + Size: pr.Size, 31 + } 32 + } 33 + 34 + type ArtistCount struct { 16 35 Pagination Pagination `json:"pagination"` 17 - From time.Time `json:"from"` 18 - To time.Time `json:"to"` 36 + Date DateRange `json:"date"` 19 37 } 20 38 21 - func (req Count) Filter() (filters.ArtistCount, error) { 39 + func (req ArtistCount) Filter() (filters.ArtistCount, error) { 22 40 filter := filters.ArtistCount{ 23 - Pagination: pagination.Filter{ 24 - Page: req.Pagination.Page, 25 - Size: req.Pagination.Size, 26 - }, 27 - From: req.From, 28 - To: req.To, 41 + Pagination: req.Pagination.Filter(), 42 + Date: req.Date.Filter(), 29 43 } 30 44 31 45 return filter, nil 32 46 } 33 47 34 48 type RecordingCount struct { 35 - Pagination Pagination `json:"pagination"` 36 - From time.Time `json:"from"` 37 - To time.Time `json:"to"` 38 - ArtistMBID string `json:"artist_mbid"` 49 + Pagination Pagination `json:"pagination"` 50 + Date DateRange `json:"date"` 51 + ArtistMBID string `json:"artist_mbid"` 52 + ReleaseMBID string `json:"release_mbid"` 39 53 } 40 54 41 55 func (req RecordingCount) Filter() (filters.RecordingCount, error) { 42 56 filter := filters.RecordingCount{ 43 - Pagination: pagination.Filter{ 44 - Page: req.Pagination.Page, 45 - Size: req.Pagination.Size, 46 - }, 47 - From: req.From, 48 - To: req.To, 49 - ArtistMBID: req.ArtistMBID, 57 + Pagination: req.Pagination.Filter(), 58 + Date: req.Date.Filter(), 59 + ArtistMBID: req.ArtistMBID, 60 + ReleaseMBID: req.ReleaseMBID, 50 61 } 51 62 52 63 return filter, nil ··· 54 65 55 66 type ReleaseCount struct { 56 67 Pagination Pagination `json:"pagination"` 57 - From time.Time `json:"from"` 58 - To time.Time `json:"to"` 68 + Date DateRange `json:"date"` 59 69 ArtistMBID string `json:"artist_mbid"` 60 70 } 61 71 62 72 func (req ReleaseCount) Filter() (filters.ReleaseCount, error) { 63 73 filter := filters.ReleaseCount{ 64 - Pagination: pagination.Filter{ 65 - Page: req.Pagination.Page, 66 - Size: req.Pagination.Size, 67 - }, 68 - From: req.From, 69 - To: req.To, 74 + Pagination: req.Pagination.Filter(), 75 + Date: req.Date.Filter(), 70 76 ArtistMBID: req.ArtistMBID, 71 77 } 72 78
+11 -15
internal/web/responses/catalog.go
··· 1 1 package responses 2 2 3 3 import ( 4 - "fmt" 5 - 6 4 "github.com/oscar345/keeptrack/internal/models" 7 5 "github.com/oscar345/keeptrack/pkg/enum" 8 6 "github.com/oscar345/keeptrack/pkg/pagination" ··· 57 55 } 58 56 59 57 type Release struct { 60 - MBID string `json:"mbid"` 61 - Name string `json:"name"` 62 - Artists []ArtistCreditName `json:"artists"` 63 - Count int `json:"count"` 58 + MBID string `json:"mbid"` 59 + Name string `json:"name"` 60 + Artists []ArtistCreditName `json:"artists"` 61 + Count int `json:"count"` 62 + ImageURL string `json:"image_url"` 64 63 } 65 64 66 65 func NewReleaseFromModel(model models.Release) Release { 67 - release := Release{ 68 - MBID: model.MBID, 69 - Name: model.Name, 70 - Artists: enum.Map(model.Artists, NewArtistCreditNameFromModel), 71 - Count: model.Count, 66 + return Release{ 67 + MBID: model.MBID, 68 + Name: model.Name, 69 + Artists: enum.Map(model.Artists, NewArtistCreditNameFromModel), 70 + Count: model.Count, 71 + ImageURL: model.ImageURL, 72 72 } 73 - 74 - fmt.Println(release) 75 - 76 - return release 77 73 } 78 74 79 75 type Page[T any] struct {
+39 -12
internal/web/router/router.go
··· 5 5 "fmt" 6 6 "log" 7 7 "net/http" 8 - "strings" 9 8 10 9 "github.com/MicahParks/keyfunc/v3" 11 10 "github.com/go-chi/chi/v5" ··· 22 21 "golang.org/x/oauth2/spotify" 23 22 ) 24 23 25 - type Services struct { 24 + type serverServices struct { 26 25 artist services.ArtistService 27 26 user services.UserService 28 27 recording services.RecordingService ··· 30 29 } 31 30 32 31 type Server struct { 33 - services Services 32 + services serverServices 34 33 config *config.Config 35 34 } 36 35 ··· 42 41 config *config.Config, 43 42 ) *Server { 44 43 return &Server{ 45 - services: Services{ 44 + services: serverServices{ 46 45 artist: artistService, 47 46 user: userService, 48 47 recording: recordingService, ··· 102 101 }) 103 102 104 103 r.Get("/{id}/count", func(w http.ResponseWriter, r *http.Request) { 105 - var request requests.Count 104 + var request requests.ArtistCount 106 105 query := r.URL.Query().Get("query") 107 106 108 107 if query != "" { 109 - fmt.Println("query string: ", query) 110 - err := json. 111 - NewDecoder(strings.NewReader(query)). 112 - Decode(&request) 113 - 114 - if err != nil { 115 - 108 + if err := json.Unmarshal([]byte(query), &request); err != nil { 116 109 http.Error(w, err.Error(), http.StatusBadRequest) 117 110 return 118 111 } ··· 158 151 render.JSON(w, r, responses.Paginate( 159 152 page, enum.Map(items, responses.NewReleaseFromModel), 160 153 )) 154 + }) 155 + 156 + r.Get("/{id}", func(w http.ResponseWriter, r *http.Request) { 157 + id := chi.URLParam(r, "id") 158 + release, err := s.services.release.GetByID(r.Context(), id) 159 + if err != nil { 160 + http.Error(w, err.Error(), http.StatusInternalServerError) 161 + return 162 + } 163 + fmt.Println("release", release) 164 + render.JSON(w, r, responses.NewReleaseFromModel(release)) 165 + }) 166 + 167 + r.Get("/{id}/count", func(w http.ResponseWriter, r *http.Request) { 168 + var request requests.ReleaseCount 169 + query := r.URL.Query().Get("query") 170 + 171 + if query != "" { 172 + if err := json.Unmarshal([]byte(query), &request); err != nil { 173 + http.Error(w, err.Error(), http.StatusBadRequest) 174 + return 175 + } 176 + } 177 + 178 + filter, _ := request.Filter() 179 + 180 + value, err := s.services.release.GetCountByID(r.Context(), chi.URLParam(r, "id"), filter) 181 + if err != nil { 182 + fmt.Println("error: ", err) 183 + http.Error(w, err.Error(), http.StatusInternalServerError) 184 + return 185 + } 186 + 187 + render.JSON(w, r, responses.NewCount(value)) 161 188 }) 162 189 } 163 190 }