mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-07 21:48:18 -08:00
* add dev branch container to workflow * correctly set the default range of ActivityGrid * fix: set name/short_name to koito (#61) * fix dev container push workflow * fix: race condition with using getComputedStyle primary color for dynamic activity grid darkening (#76) * Fix race condition with using getComputedStyle primary color for dynamic activity grid darkening Instead just use the color from the current theme directly. Tested works on initial load and theme changes. Fixes https://github.com/gabehf/Koito/issues/75 * Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name Split name out of the Theme struct to simplify custom theme saving/reading * fix: set first artist listed as primary by default (#81) * feat: add server-side configuration with default theme (#90) * docs: add example for usage of the main listenbrainz instance (#71) * docs: add example for usage of the main listenbrainz instance * Update scrobbler.md --------- Co-authored-by: Gabe Farrell <90876006+gabehf@users.noreply.github.com> * feat: add server-side cfg and default theme * fix: repair custom theme --------- Co-authored-by: m0d3rnX <jesper@posteo.de> * docs: add default theme cfg option to docs * feat: add ability to manually scrobble track (#91) * feat: add button to manually scrobble from ui * fix: ensure timestamp is in the past, log fix * test: add integration test * feat: add first listened to dates for media items (#92) * fix: ensure error checks for ErrNoRows * feat: add now playing endpoint and ui (#93) * wip * feat: add now playing * fix: set default theme when config is not set * feat: fetch images from subsonic server (#94) * fix: useQuery instead of useEffect for now playing * feat: custom artist separator regex (#95) * Fix race condition with using getComputedStyle primary color for dynamic activity grid darkening Instead just use the color from the current theme directly. Tested works on initial load and theme changes. Fixes https://github.com/gabehf/Koito/issues/75 * Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name Split name out of the Theme struct to simplify custom theme saving/reading * feat: add server-side configuration with default theme (#90) * docs: add example for usage of the main listenbrainz instance (#71) * docs: add example for usage of the main listenbrainz instance * Update scrobbler.md --------- Co-authored-by: Gabe Farrell <90876006+gabehf@users.noreply.github.com> * feat: add server-side cfg and default theme * fix: repair custom theme --------- Co-authored-by: m0d3rnX <jesper@posteo.de> * fix: rebase errors --------- Co-authored-by: pet <128837728+againstpetra@users.noreply.github.com> Co-authored-by: mlandry <mike.landry@gmail.com> Co-authored-by: m0d3rnX <jesper@posteo.de>
374 lines
12 KiB
Go
374 lines
12 KiB
Go
package catalog_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"regexp"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gabehf/koito/internal/catalog"
|
|
"github.com/gabehf/koito/internal/cfg"
|
|
"github.com/gabehf/koito/internal/db/psql"
|
|
"github.com/gabehf/koito/internal/mbz"
|
|
"github.com/gabehf/koito/internal/utils"
|
|
_ "github.com/gabehf/koito/testing_init"
|
|
"github.com/google/uuid"
|
|
"github.com/ory/dockertest/v3"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var (
|
|
mbzArtistData = map[uuid.UUID]*mbz.MusicBrainzArtist{
|
|
uuid.MustParse("00000000-0000-0000-0000-000000000001"): {
|
|
Name: "ATARASHII GAKKO!",
|
|
SortName: "Atarashii Gakko",
|
|
Aliases: []mbz.MusicBrainzArtistAlias{
|
|
{
|
|
Name: "新しい学校のリーダーズ",
|
|
Type: "Artist name",
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
mbzReleaseGroupData = map[uuid.UUID]*mbz.MusicBrainzReleaseGroup{
|
|
uuid.MustParse("00000000-0000-0000-0000-000000000011"): {
|
|
Title: "AG! Calling",
|
|
Type: "Album",
|
|
ArtistCredit: []mbz.MusicBrainzArtistCredit{
|
|
{
|
|
Artist: mbz.MusicBrainzArtist{
|
|
Name: "ATARASHII GAKKO!",
|
|
Aliases: []mbz.MusicBrainzArtistAlias{
|
|
{
|
|
Name: "新しい学校のリーダーズ",
|
|
Type: "Artist name",
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
Name: "ATARASHII GAKKO!",
|
|
},
|
|
},
|
|
Releases: []mbz.MusicBrainzRelease{
|
|
{
|
|
Title: "AG! Calling",
|
|
ID: "00000000-0000-0000-0000-000000000101",
|
|
ArtistCredit: []mbz.MusicBrainzArtistCredit{
|
|
{
|
|
Artist: mbz.MusicBrainzArtist{
|
|
Name: "ATARASHII GAKKO!",
|
|
Aliases: []mbz.MusicBrainzArtistAlias{
|
|
{
|
|
Name: "ATARASHII GAKKO!",
|
|
Type: "Artist name",
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
Name: "ATARASHII GAKKO!",
|
|
},
|
|
},
|
|
Status: "Official",
|
|
},
|
|
{
|
|
Title: "AG! Calling - Alt Title",
|
|
ID: "00000000-0000-0000-0000-000000000102",
|
|
ArtistCredit: []mbz.MusicBrainzArtistCredit{
|
|
{
|
|
Artist: mbz.MusicBrainzArtist{
|
|
Name: "ATARASHII GAKKO!",
|
|
Aliases: []mbz.MusicBrainzArtistAlias{
|
|
{
|
|
Name: "ATARASHII GAKKO!",
|
|
Type: "Artist name",
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
Name: "ATARASHII GAKKO!",
|
|
},
|
|
},
|
|
Status: "Official",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
mbzReleaseData = map[uuid.UUID]*mbz.MusicBrainzRelease{
|
|
uuid.MustParse("00000000-0000-0000-0000-000000000101"): {
|
|
Title: "AG! Calling",
|
|
ID: "00000000-0000-0000-0000-000000000101",
|
|
ArtistCredit: []mbz.MusicBrainzArtistCredit{
|
|
{
|
|
Artist: mbz.MusicBrainzArtist{
|
|
Name: "ATARASHII GAKKO!",
|
|
Aliases: []mbz.MusicBrainzArtistAlias{
|
|
{
|
|
Name: "新しい学校のリーダーズ",
|
|
Type: "Artist name",
|
|
Primary: true,
|
|
},
|
|
},
|
|
},
|
|
Name: "ATARASHII GAKKO!",
|
|
},
|
|
},
|
|
Status: "Official",
|
|
},
|
|
uuid.MustParse("00000000-0000-0000-0000-000000000202"): {
|
|
Title: "EVANGELION FINALLY",
|
|
ID: "00000000-0000-0000-0000-000000000202",
|
|
ArtistCredit: []mbz.MusicBrainzArtistCredit{
|
|
{
|
|
Artist: mbz.MusicBrainzArtist{
|
|
Name: "Various Artists",
|
|
},
|
|
Name: "Various Artists",
|
|
},
|
|
},
|
|
Status: "Official",
|
|
},
|
|
}
|
|
mbzTrackData = map[uuid.UUID]*mbz.MusicBrainzTrack{
|
|
uuid.MustParse("00000000-0000-0000-0000-000000001001"): {
|
|
Title: "Tokyo Calling",
|
|
LengthMs: 191000,
|
|
},
|
|
}
|
|
)
|
|
|
|
var store *psql.Psql
|
|
|
|
func getTestGetenv(resource *dockertest.Resource) func(string) string {
|
|
dir, err := utils.GenerateRandomString(8)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return func(env string) string {
|
|
switch env {
|
|
case cfg.ENABLE_STRUCTURED_LOGGING_ENV:
|
|
return "true"
|
|
case cfg.LOG_LEVEL_ENV:
|
|
return "debug"
|
|
case cfg.DATABASE_URL_ENV:
|
|
return fmt.Sprintf("postgres://postgres:secret@localhost:%s", resource.GetPort("5432/tcp"))
|
|
case cfg.CONFIG_DIR_ENV:
|
|
return dir
|
|
case cfg.DISABLE_DEEZER_ENV, cfg.DISABLE_COVER_ART_ARCHIVE_ENV, cfg.DISABLE_MUSICBRAINZ_ENV, cfg.ENABLE_FULL_IMAGE_CACHE_ENV:
|
|
return "true"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
}
|
|
|
|
func truncateTestData(t *testing.T) {
|
|
err := store.Exec(context.Background(),
|
|
`TRUNCATE
|
|
artists,
|
|
artist_aliases,
|
|
tracks,
|
|
artist_tracks,
|
|
releases,
|
|
artist_releases,
|
|
release_aliases,
|
|
listens
|
|
RESTART IDENTITY CASCADE`)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func setupTestDataWithMbzIDs(t *testing.T) {
|
|
truncateTestData(t)
|
|
|
|
err := store.Exec(context.Background(),
|
|
`INSERT INTO artists (musicbrainz_id)
|
|
VALUES ('00000000-0000-0000-0000-000000000001')`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_aliases (artist_id, alias, source, is_primary)
|
|
VALUES (1, 'ATARASHII GAKKO!', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO releases (musicbrainz_id)
|
|
VALUES ('00000000-0000-0000-0000-000000000101')`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO release_aliases (release_id, alias, source, is_primary)
|
|
VALUES (1, 'AG! Calling', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_releases (artist_id, release_id)
|
|
VALUES (1, 1)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO tracks (release_id, musicbrainz_id)
|
|
VALUES (1, '00000000-0000-0000-0000-000000001001')`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO track_aliases (track_id, alias, source, is_primary)
|
|
VALUES (1, 'Tokyo Calling', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_tracks (artist_id, track_id)
|
|
VALUES (1, 1)`)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func setupTestDataSansMbzIDs(t *testing.T) {
|
|
truncateTestData(t)
|
|
|
|
err := store.Exec(context.Background(),
|
|
`INSERT INTO artists (musicbrainz_id)
|
|
VALUES (NULL)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_aliases (artist_id, alias, source, is_primary)
|
|
VALUES (1, 'ATARASHII GAKKO!', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO releases (musicbrainz_id)
|
|
VALUES (NULL)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO release_aliases (release_id, alias, source, is_primary)
|
|
VALUES (1, 'AG! Calling', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_releases (artist_id, release_id)
|
|
VALUES (1, 1)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO tracks (release_id, musicbrainz_id)
|
|
VALUES (1, NULL)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO track_aliases (track_id, alias, source, is_primary)
|
|
VALUES (1, 'Tokyo Calling', 'Testing', true)`)
|
|
require.NoError(t, err)
|
|
err = store.Exec(context.Background(),
|
|
`INSERT INTO artist_tracks (artist_id, track_id)
|
|
VALUES (1, 1)`)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
pool, err := dockertest.NewPool("")
|
|
if err != nil {
|
|
log.Fatalf("Could not construct pool: %s", err)
|
|
}
|
|
|
|
if err := pool.Client.Ping(); err != nil {
|
|
log.Fatalf("Could not connect to Docker: %s", err)
|
|
}
|
|
|
|
resource, err := pool.Run("postgres", "latest", []string{"POSTGRES_PASSWORD=secret"})
|
|
if err != nil {
|
|
log.Fatalf("Could not start resource: %s", err)
|
|
}
|
|
|
|
err = cfg.Load(getTestGetenv(resource), "test")
|
|
if err != nil {
|
|
log.Fatalf("Could not load cfg: %s", err)
|
|
}
|
|
|
|
if err := pool.Retry(func() error {
|
|
var err error
|
|
store, err = psql.New()
|
|
if err != nil {
|
|
log.Println("Failed to connect to test database, retrying...")
|
|
return err
|
|
}
|
|
return store.Ping(context.Background())
|
|
}); err != nil {
|
|
log.Fatalf("Could not connect to database: %s", err)
|
|
}
|
|
|
|
// insert a user into the db with id 1 to use for tests
|
|
err = store.Exec(context.Background(), `INSERT INTO users (username, password) VALUES ('test', DECODE('abc123', 'hex'))`)
|
|
if err != nil {
|
|
log.Fatalf("Failed to insert test user: %v", err)
|
|
}
|
|
|
|
code := m.Run()
|
|
|
|
// You can't defer this because os.Exit doesn't care for defer
|
|
if err := pool.Purge(resource); err != nil {
|
|
log.Fatalf("Could not purge resource: %s", err)
|
|
}
|
|
|
|
err = os.RemoveAll(cfg.ConfigDir())
|
|
if err != nil {
|
|
log.Fatalf("Could not remove temporary config dir: %v", err)
|
|
}
|
|
|
|
os.Exit(code)
|
|
}
|
|
|
|
// From: https://brandur.org/fragments/go-equal-time
|
|
// EqualTime compares two times in a way that's safer and with better fail
|
|
// output than a call to `require.Equal` would produce.
|
|
//
|
|
// It takes care to:
|
|
//
|
|
// - Strip off monotonic portions of timestamps so they aren't considered for
|
|
// purposes of comparison.
|
|
//
|
|
// - Truncate nanoseconds in a functionally equivalent way to how pgx would do
|
|
// it so that times that have round-tripped from Postgres can still be
|
|
// compared. Postgres only stores times to the microsecond level.
|
|
//
|
|
// - Use formatted, human-friendly time outputs so that in case of a failure,
|
|
// the discrepancy is easier to pick out.
|
|
func EqualTime(t testing.TB, t1, t2 time.Time) {
|
|
// Note that leaving off the nanosecond portion will have the effect of
|
|
// truncating it rather than rounding to the nearest microsecond, which
|
|
// functionally matches pgx's behavior while persisting.
|
|
const rfc3339Micro = "2006-01-02T15:04:05.999999Z07:00"
|
|
|
|
require.Equal(t,
|
|
t1.Format(rfc3339Micro),
|
|
t2.Format(rfc3339Micro),
|
|
)
|
|
}
|
|
|
|
func TestArtistStringParse(t *testing.T) {
|
|
type input struct {
|
|
Name string
|
|
Title string
|
|
}
|
|
cases := map[input][]string{
|
|
// only one artist
|
|
{"NELKE", ""}: {"NELKE"},
|
|
{"The Brook & The Bluff", ""}: {"The Brook & The Bluff"},
|
|
{"half·alive", ""}: {"half·alive"},
|
|
// Earth, Wind, & Fire
|
|
{"Earth, Wind & Fire", "The Very Best of Earth, Wind & Fire"}: {"Earth, Wind & Fire"},
|
|
// only artists in artist string
|
|
{"Carly Rae Jepsen feat. Rufus Wainwright", ""}: {"Carly Rae Jepsen", "Rufus Wainwright"},
|
|
{"Mimi (feat. HATSUNE MIKU & KAFU)", ""}: {"Mimi", "HATSUNE MIKU", "KAFU"},
|
|
{"Magnify Tokyo · Kanade Ishihara", ""}: {"Magnify Tokyo", "Kanade Ishihara"},
|
|
{"Daft Punk [feat. Paul Williams]", ""}: {"Daft Punk", "Paul Williams"},
|
|
// primary artist in artist string, features in title
|
|
{"Tyler, The Creator", "CA (feat. Alice Smith, Leon Ware & Clem Creevy)"}: {"Tyler, The Creator", "Alice Smith", "Leon Ware", "Clem Creevy"},
|
|
{"ONE OK ROCK", "C.U.R.I.O.S.I.T.Y. (feat. Paledusk and CHICO CARLITO)"}: {"ONE OK ROCK", "Paledusk", "CHICO CARLITO"},
|
|
{"Rat Tally", "In My Car feat. Madeline Kenney"}: {"Rat Tally", "Madeline Kenney"},
|
|
// artists in both
|
|
{"Daft Punk feat. Julian Casablancas", "Instant Crush (feat. Julian Casablancas)"}: {"Daft Punk", "Julian Casablancas"},
|
|
{"Paramore (feat. Joy Williams)", "Hate to See Your Heart Break feat. Joy Williams"}: {"Paramore", "Joy Williams"},
|
|
{"MINSU", "오해 금지 (Feat. BIG Naughty)"}: {"MINSU", "BIG Naughty"},
|
|
{"MINSU", "오해 금지 [Feat. BIG Naughty]"}: {"MINSU", "BIG Naughty"},
|
|
{"MINSU", "오해 금지 Feat. BIG Naughty"}: {"MINSU", "BIG Naughty"},
|
|
|
|
// custom separator
|
|
{"MIMiNARI//楠木ともり", "眠れない"}: {"MIMiNARI", "楠木ともり"},
|
|
}
|
|
|
|
for in, out := range cases {
|
|
artists := catalog.ParseArtists(in.Name, in.Title, []*regexp.Regexp{regexp.MustCompile(`\s*//\s*`), regexp.MustCompile(`\s+·\s+`)})
|
|
assert.ElementsMatch(t, out, artists)
|
|
}
|
|
}
|