package subsonic import ( "context" "database/sql" "encoding/json" "time" "git.noroutes.net/NoRoutes/vibetunes/internal/database" "git.noroutes.net/NoRoutes/vibetunes/internal/metadataenrich" ) // enrichmentCacheTTL is how long a cached MusicBrainz/Cover Art Archive // response is trusted before VibeSonic tries fetching again. This data // changes rarely, so a long TTL avoids hammering the (rate-limited, // shared, free) MusicBrainz service on every request. const enrichmentCacheTTL = 30 * 24 * time.Hour var enrichClient = &metadataenrich.Client{} // artistInfoBase builds the local ArtistInfoBase for an artist ID, // filling in the MusicBrainz-derived fields only when external // metadata is enabled, the artist has a tagged MusicBrainz ID, and a // (possibly cached) lookup actually succeeds. Any failure along the // way — unknown id, no mbid, network error, disabled feature — leaves // those fields blank rather than erroring the whole request or // inventing a value. func artistInfoBase(rc *Context, id string) ArtistInfoBase { db := rc.Server.DB artist, err := database.GetArtist(db, id) if err != nil { return ArtistInfoBase{} } base := ArtistInfoBase{MusicBrainzID: artist.MusicBrainzID} if !rc.Server.Config.EnableExternalMetadata || artist.MusicBrainzID == "" { return base } data, err := fetchArtistCached(rc.R.Context(), db, artist.MusicBrainzID) if err != nil || data == nil { return base } base.Biography = data.Disambiguation return base } // albumInfo builds the local AlbumInfo for an album ID, enriched with // Cover Art Archive image URLs under the same conditions as // artistInfoBase. func albumInfo(rc *Context, id string) *AlbumInfo { db := rc.Server.DB album, err := database.GetAlbum(db, id) if err != nil { return &AlbumInfo{} } info := &AlbumInfo{MusicBrainzID: album.MusicBrainzID} if !rc.Server.Config.EnableExternalMetadata || album.MusicBrainzID == "" { return info } art, err := fetchCoverArtCached(rc.R.Context(), db, album.MusicBrainzID) if err != nil || art == nil { return info } info.SmallImageURL = art.SmallURL info.MediumImageURL = art.MediumURL info.LargeImageURL = art.LargeURL return info } func fetchArtistCached(ctx context.Context, db *sql.DB, mbid string) (*metadataenrich.ArtistData, error) { if cached, ok := readEnrichmentCache(db, "artist", mbid, "musicbrainz"); ok { var data metadataenrich.ArtistData if err := json.Unmarshal([]byte(cached), &data); err == nil { return &data, nil } } data, err := enrichClient.FetchArtist(ctx, mbid) if err != nil { return nil, err } if raw, err := json.Marshal(data); err == nil { _ = database.PutCachedExternalMetadata(db, "artist", mbid, "musicbrainz", string(raw)) } return data, nil } func fetchCoverArtCached(ctx context.Context, db *sql.DB, mbid string) (*metadataenrich.CoverArt, error) { if cached, ok := readEnrichmentCache(db, "release", mbid, "coverartarchive"); ok { var art metadataenrich.CoverArt if err := json.Unmarshal([]byte(cached), &art); err == nil { return &art, nil } } art, err := enrichClient.FetchReleaseCoverArt(ctx, mbid) if err != nil { return nil, err } if art != nil { if raw, err := json.Marshal(art); err == nil { _ = database.PutCachedExternalMetadata(db, "release", mbid, "coverartarchive", string(raw)) } } return art, nil } // readEnrichmentCache returns a cached response's JSON body if one // exists and is still within enrichmentCacheTTL. func readEnrichmentCache(db *sql.DB, entityType, mbid, source string) (string, bool) { dataJSON, fetchedAt, found, err := database.GetCachedExternalMetadata(db, entityType, mbid, source) if err != nil || !found { return "", false } if time.Since(fetchedAt) > enrichmentCacheTTL { return "", false } return dataJSON, true }