package model
import (
"context"
"iter"
"math"
"sync"
"time"
"github.com/deluan/rest"
"github.com/gohugoio/hashstructure"
"github.com/navidrome/navidrome/conf"
)
type Album struct {
Annotations `structs:"-" hash:"ignore"`
ItemImage `structs:"-" hash:"ignore"`
ID string `structs:"id" json:"id"`
LibraryID int `structs:"library_id" json:"libraryId"`
LibraryPath string `structs:"-" json:"libraryPath" hash:"ignore"`
LibraryName string `structs:"-" json:"libraryName" hash:"ignore"`
Name string `structs:"name" json:"name"`
EmbedArtPath string `structs:"embed_art_path" json:"-"`
AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId"`
AlbumArtist string `structs:"album_artist" json:"albumArtist"`
MaxYear int `structs:"max_year" json:"maxYear"`
MinYear int `structs:"min_year" json:"minYear"`
Date string `structs:"date" json:"date,omitempty"`
MaxOriginalYear int `structs:"max_original_year" json:"maxOriginalYear"`
MinOriginalYear int `structs:"min_original_year" json:"minOriginalYear"`
OriginalDate string `structs:"original_date" json:"originalDate,omitempty"`
ReleaseDate string `structs:"release_date" json:"releaseDate,omitempty"`
Compilation bool `structs:"compilation" json:"compilation"`
Comment string `structs:"comment" json:"comment,omitempty"`
SongCount int `structs:"song_count" json:"songCount"`
Duration float32 `structs:"duration" json:"duration"`
Size int64 `structs:"size" json:"size"`
Discs Discs `structs:"discs" json:"discs,omitempty"`
SortAlbumName string `structs:"sort_album_name" json:"sortAlbumName,omitempty"`
SortAlbumArtistName string `structs:"sort_album_artist_name" json:"sortAlbumArtistName,omitempty"`
OrderAlbumName string `structs:"order_album_name" json:"orderAlbumName"`
OrderAlbumArtistName string `structs:"order_album_artist_name" json:"orderAlbumArtistName"`
CatalogNum string `structs:"catalog_num" json:"catalogNum,omitempty"`
MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty"`
MbzAlbumArtistID string `structs:"mbz_album_artist_id" json:"mbzAlbumArtistId,omitempty"`
MbzAlbumType string `structs:"mbz_album_type" json:"mbzAlbumType,omitempty"`
MbzAlbumComment string `structs:"mbz_album_comment" json:"mbzAlbumComment,omitempty"`
MbzReleaseGroupID string `structs:"mbz_release_group_id" json:"mbzReleaseGroupId,omitempty"`
FolderIDs []string `structs:"folder_ids" json:"-" hash:"set"`
ExplicitStatus string `structs:"explicit_status" json:"explicitStatus"`
RGAlbumGain *float64 `structs:"rg_album_gain" json:"rgAlbumGain"`
RGAlbumPeak *float64 `structs:"rg_album_peak" json:"rgAlbumPeak"`
Description string `structs:"description" json:"description,omitempty" hash:"ignore"`
SmallImageUrl string `structs:"small_image_url" json:"smallImageUrl,omitempty" hash:"ignore"`
MediumImageUrl string `structs:"medium_image_url" json:"mediumImageUrl,omitempty" hash:"ignore"`
LargeImageUrl string `structs:"large_image_url" json:"largeImageUrl,omitempty" hash:"ignore"`
ExternalUrl string `structs:"external_url" json:"externalUrl,omitempty" hash:"ignore"`
ExternalInfoUpdatedAt *time.Time `structs:"external_info_updated_at" json:"externalInfoUpdatedAt" hash:"ignore"`
Genre string `structs:"genre" json:"genre" hash:"ignore"`
Genres Genres `structs:"-" json:"genres" hash:"ignore"`
Tags Tags `structs:"tags" json:"tags,omitempty" hash:"ignore"`
Participants Participants `structs:"participants" json:"participants" hash:"ignore"`
Missing bool `structs:"missing" json:"missing"`
ImportedAt time.Time `structs:"imported_at" json:"importedAt" hash:"ignore"`
CreatedAt time.Time `structs:"created_at" json:"createdAt"`
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"`
}
func (a Album) CoverArtID() ArtworkID {
return artworkIDFromAlbum(a)
}
func (a Album) FullName() string {
if v := a.Tags.First(TagAlbumVersion); conf.Server.Subsonic.AppendAlbumVersion && v != "" {
return appendSuffix(a.Name, v)
}
return a.Name
}
func (a Album) Equals(other Album) bool {
a.Duration = float32(math.Floor(float64(a.Duration)))
other.Duration = float32(math.Floor(float64(other.Duration)))
opts := &hashstructure.HashOptions{
IgnoreZeroValue: true,
ZeroNil: true,
}
hash1, _ := hashstructure.Hash(a, opts)
hash2, _ := hashstructure.Hash(other, opts)
return hash1 == hash2
}
var AlbumLevelTags = sync.OnceValue(func() map[TagName]struct{} {
tags := make(map[TagName]struct{})
m := TagMappings()
for t, conf := range m {
if conf.Album {
tags[t] = struct{}{}
}
}
return tags
})
func (a *Album) SetTags(tags TagList) {
a.Tags = tags.GroupByFrequency()
for k := range a.Tags {
if _, ok := AlbumLevelTags()[k]; !ok {
delete(a.Tags, k)
}
}
}
type Discs map[int]string
func (d Discs) Add(discNumber int, discSubtitle string) {
d[discNumber] = discSubtitle
}
type DiscID struct {
AlbumID string `json:"albumId"`
ReleaseDate string `json:"releaseDate"`
DiscNumber int `json:"discNumber"`
}
type Albums []Album
type AlbumCursor iter.Seq2[Album, error]
type AlbumRepository interface {
rest.Repository[Album]
CountAll(ctx context.Context, options ...QueryOptions) (int64, error)
Exists(ctx context.Context, id string) (bool, error)
Put(ctx context.Context, m *Album) error
UpdateExternalInfo(ctx context.Context, m *Album) error
Get(ctx context.Context, id string) (*Album, error)
GetAll(ctx context.Context, options ...QueryOptions) (Albums, error)
GetSoleAlbumArtistIDsInSubtrees(ctx context.Context, lib Library, paths ...string) ([]string, error)
GetCursor(ctx context.Context, options ...QueryOptions) (AlbumCursor, error)
GetYears(ctx context.Context, libraryIDs ...int) ([]int, error)
Touch(ctx context.Context, ids ...string) error
TouchByMissingFolder(ctx context.Context) (int64, error)
GetTouchedAlbums(ctx context.Context, libID int) (AlbumCursor, error)
RefreshPlayCounts(ctx context.Context) (int64, error)
CopyAttributes(ctx context.Context, fromID, toID string, columns ...string) error
AnnotatedRepository
SearchableRepository[Albums]
}