Add discs to album

This commit is contained in:
Deluan 2023-12-08 19:29:16 -05:00
parent 0ca0d5da22
commit af7eead037
7 changed files with 165 additions and 14 deletions

View File

@ -0,0 +1,36 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigrationContext(upAddDiscToAlbum, downAddDiscToAlbum)
}
func upAddDiscToAlbum(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `alter table album add discs JSONB default '{}';`)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `
update album set discs = t.discs
from (select album_id, json_group_object(disc_number, disc_subtitle) as discs
from (select distinct album_id, disc_number, disc_subtitle
from media_file
where disc_number > 0
order by album_id, disc_number)
group by album_id
having discs <> '{"1":""}') as t
where album.id = t.album_id;
`)
return err
}
func downAddDiscToAlbum(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `alter table album drop discs;`)
return err
}

View File

@ -1,6 +1,7 @@
package model
import (
"strconv"
"time"
"github.com/navidrome/navidrome/utils/slice"
@ -33,6 +34,7 @@ type Album struct {
Size int64 `structs:"size" json:"size"`
Genre string `structs:"genre" json:"genre"`
Genres Genres `structs:"-" json:"genres"`
Discs Discs `structs:"discs" json:"discs,omitempty"`
FullText string `structs:"full_text" json:"fullText"`
SortAlbumName string `structs:"sort_album_name" json:"sortAlbumName,omitempty"`
SortArtistName string `structs:"sort_artist_name" json:"sortArtistName,omitempty"`
@ -60,6 +62,15 @@ func (a Album) CoverArtID() ArtworkID {
return artworkIDFromAlbum(a)
}
type Discs map[string]string
func (d *Discs) Add(discNumber int, discSubtitle string) {
if *d == nil {
*d = Discs{}
}
(*d)[strconv.Itoa(discNumber)] = discSubtitle
}
type DiscID struct {
AlbumID string `json:"albumId"`
ReleaseDate string `json:"releaseDate"`

View File

@ -160,6 +160,9 @@ func (mfs MediaFiles) ToAlbum() Album {
if m.HasCoverArt && a.EmbedArtPath == "" {
a.EmbedArtPath = m.Path
}
if m.DiscNumber > 0 {
a.Discs.Add(m.DiscNumber, m.DiscSubtitle)
}
}
a.Paths = strings.Join(mfs.Dirs(), consts.Zwsp)

View File

@ -123,6 +123,36 @@ var _ = Describe("MediaFiles", func() {
})
})
Context("Calculated attributes", func() {
Context("Discs", func() {
When("we have no discs", func() {
BeforeEach(func() {
mfs = MediaFiles{{Album: "Album1"}, {Album: "Album1"}, {Album: "Album1"}}
})
It("sets the correct Discs", func() {
album := mfs.ToAlbum()
Expect(album.Discs).To(BeEmpty())
})
})
When("we have only one disc", func() {
BeforeEach(func() {
mfs = MediaFiles{{DiscNumber: 1, DiscSubtitle: "DiscSubtitle"}}
})
It("sets the correct Discs", func() {
album := mfs.ToAlbum()
Expect(album.Discs).To(Equal(Discs{"1": "DiscSubtitle"}))
})
})
When("we have multiple discs", func() {
BeforeEach(func() {
mfs = MediaFiles{{DiscNumber: 1, DiscSubtitle: "DiscSubtitle"}, {DiscNumber: 2, DiscSubtitle: "DiscSubtitle2"}, {DiscNumber: 1, DiscSubtitle: "DiscSubtitle"}}
})
It("sets the correct Discs", func() {
album := mfs.ToAlbum()
Expect(album.Discs).To(Equal(Discs{"1": "DiscSubtitle", "2": "DiscSubtitle2"}))
})
})
})
Context("Genres", func() {
When("we have only one Genre", func() {
BeforeEach(func() {

View File

@ -2,6 +2,7 @@ package persistence
import (
"context"
"encoding/json"
"fmt"
"strings"
@ -18,6 +19,32 @@ type albumRepository struct {
sqlRestful
}
type dbAlbum struct {
*model.Album `structs:",flatten"`
Discs string `structs:"-" json:"discs"`
}
func (a *dbAlbum) PostScan() error {
if a.Discs == "" {
a.Album.Discs = model.Discs{}
return nil
}
return json.Unmarshal([]byte(a.Discs), &a.Album.Discs)
}
func (a *dbAlbum) PostMapArgs(m map[string]any) error {
if len(a.Album.Discs) == 0 {
m["discs"] = "{}"
return nil
}
b, err := json.Marshal(a.Album.Discs)
if err != nil {
return err
}
m["discs"] = string(b)
return nil
}
func NewAlbumRepository(ctx context.Context, db dbx.Builder) model.AlbumRepository {
r := &albumRepository{}
r.ctx = ctx
@ -102,19 +129,20 @@ func (r *albumRepository) selectAlbum(options ...model.QueryOptions) SelectBuild
func (r *albumRepository) Get(id string) (*model.Album, error) {
sq := r.selectAlbum().Where(Eq{"album.id": id})
var res model.Albums
if err := r.queryAll(sq, &res); err != nil {
var dba []dbAlbum
if err := r.queryAll(sq, &dba); err != nil {
return nil, err
}
if len(res) == 0 {
if len(dba) == 0 {
return nil, model.ErrNotFound
}
res := r.toModels(dba)
err := r.loadAlbumGenres(&res)
return &res[0], err
}
func (r *albumRepository) Put(m *model.Album) error {
_, err := r.put(m.ID, m)
_, err := r.put(m.ID, &dbAlbum{Album: m})
if err != nil {
return err
}
@ -130,14 +158,22 @@ func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, e
return res, err
}
func (r *albumRepository) toModels(dba []dbAlbum) model.Albums {
res := model.Albums{}
for i := range dba {
res = append(res, *dba[i].Album)
}
return res
}
func (r *albumRepository) GetAllWithoutGenres(options ...model.QueryOptions) (model.Albums, error) {
sq := r.selectAlbum(options...)
res := model.Albums{}
err := r.queryAll(sq, &res)
var dba []dbAlbum
err := r.queryAll(sq, &dba)
if err != nil {
return nil, err
}
return res, err
return r.toModels(dba), err
}
func (r *albumRepository) purgeEmpty() error {
@ -152,13 +188,14 @@ func (r *albumRepository) purgeEmpty() error {
}
func (r *albumRepository) Search(q string, offset int, size int) (model.Albums, error) {
results := model.Albums{}
err := r.doSearch(q, offset, size, &results, "name")
var dba []dbAlbum
err := r.doSearch(q, offset, size, &dba, "name")
if err != nil {
return nil, err
}
err = r.loadAlbumGenres(&results)
return results, err
res := r.toModels(dba)
err = r.loadAlbumGenres(&res)
return res, err
}
func (r *albumRepository) Count(options ...rest.QueryOptions) (int64, error) {

View File

@ -3,6 +3,7 @@ package persistence
import (
"context"
"github.com/fatih/structs"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
@ -55,4 +56,37 @@ var _ = Describe("AlbumRepository", func() {
}))
})
})
Describe("dbAlbum mapping", func() {
var a *model.Album
BeforeEach(func() {
a = &model.Album{ID: "1", Name: "name", ArtistID: "2"}
})
It("maps empty discs field", func() {
a.Discs = model.Discs{}
dba := dbAlbum{Album: a}
m := structs.Map(dba)
Expect(dba.PostMapArgs(m)).To(Succeed())
Expect(m).To(HaveKeyWithValue("discs", `{}`))
other := dbAlbum{Album: &model.Album{ID: "1", Name: "name"}, Discs: "{}"}
Expect(other.PostScan()).To(Succeed())
Expect(other.Album.Discs).To(Equal(a.Discs))
})
It("maps the discs field", func() {
a.Discs = model.Discs{"1": "disc1", "2": "disc2"}
dba := dbAlbum{Album: a}
m := structs.Map(dba)
Expect(dba.PostMapArgs(m)).To(Succeed())
Expect(m).To(HaveKeyWithValue("discs", `{"1":"disc1","2":"disc2"}`))
other := dbAlbum{Album: &model.Album{ID: "1", Name: "name"}, Discs: m["discs"].(string)}
Expect(other.PostScan()).To(Succeed())
Expect(other.Album.Discs).To(Equal(a.Discs))
})
})
})

View File

@ -49,9 +49,9 @@ var (
)
var (
albumSgtPeppers = model.Album{ID: "101", Name: "Sgt Peppers", Artist: "The Beatles", OrderAlbumName: "sgt peppers", AlbumArtistID: "3", Genre: "Rock", Genres: model.Genres{genreRock}, EmbedArtPath: P("/beatles/1/sgt/a day.mp3"), SongCount: 1, MaxYear: 1967, FullText: " beatles peppers sgt the"}
albumAbbeyRoad = model.Album{ID: "102", Name: "Abbey Road", Artist: "The Beatles", OrderAlbumName: "abbey road", AlbumArtistID: "3", Genre: "Rock", Genres: model.Genres{genreRock}, EmbedArtPath: P("/beatles/1/come together.mp3"), SongCount: 1, MaxYear: 1969, FullText: " abbey beatles road the"}
albumRadioactivity = model.Album{ID: "103", Name: "Radioactivity", Artist: "Kraftwerk", OrderAlbumName: "radioactivity", AlbumArtistID: "2", Genre: "Electronic", Genres: model.Genres{genreElectronic, genreRock}, EmbedArtPath: P("/kraft/radio/radio.mp3"), SongCount: 2, FullText: " kraftwerk radioactivity"}
albumSgtPeppers = model.Album{ID: "101", Name: "Sgt Peppers", Artist: "The Beatles", OrderAlbumName: "sgt peppers", AlbumArtistID: "3", Genre: "Rock", Genres: model.Genres{genreRock}, EmbedArtPath: P("/beatles/1/sgt/a day.mp3"), SongCount: 1, MaxYear: 1967, FullText: " beatles peppers sgt the", Discs: model.Discs{}}
albumAbbeyRoad = model.Album{ID: "102", Name: "Abbey Road", Artist: "The Beatles", OrderAlbumName: "abbey road", AlbumArtistID: "3", Genre: "Rock", Genres: model.Genres{genreRock}, EmbedArtPath: P("/beatles/1/come together.mp3"), SongCount: 1, MaxYear: 1969, FullText: " abbey beatles road the", Discs: model.Discs{}}
albumRadioactivity = model.Album{ID: "103", Name: "Radioactivity", Artist: "Kraftwerk", OrderAlbumName: "radioactivity", AlbumArtistID: "2", Genre: "Electronic", Genres: model.Genres{genreElectronic, genreRock}, EmbedArtPath: P("/kraft/radio/radio.mp3"), SongCount: 2, FullText: " kraftwerk radioactivity", Discs: model.Discs{}}
testAlbums = model.Albums{
albumSgtPeppers,
albumAbbeyRoad,