Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1491,6 +1491,65 @@ paths:
default:
$ref: "#/components/responses/Error"

/resolve/artist:
get:
operationId: resolveArtist
summary: Find an artist on a service you can use
description: |
For a name that came from somewhere you can't browse (a song queued
from someone else's service, or one without an artist ID). Looks
for the artist by name on your links, then on shared ones, and
returns the first exact match. 404 when none has it.
tags: [browse]
parameters:
- name: name
in: query
required: true
schema:
type: string
minLength: 1
maxLength: 200
responses:
"200":
description: Where to open the artist
content:
application/json:
schema:
$ref: "#/components/schemas/ResolvedRef"
default:
$ref: "#/components/responses/Error"

/resolve/album:
get:
operationId: resolveAlbum
summary: Find an album on a service you can use
description: |
Like `resolveArtist`, for an album by its title and, when known, its
artist.
tags: [browse]
parameters:
- name: title
in: query
required: true
schema:
type: string
minLength: 1
maxLength: 200
- name: artist
in: query
schema:
type: string
maxLength: 200
responses:
"200":
description: Where to open the album
content:
application/json:
schema:
$ref: "#/components/schemas/ResolvedRef"
default:
$ref: "#/components/responses/Error"

/links/{id}/artists/{artistId}/shuffle:
parameters:
- name: id
Expand Down Expand Up @@ -7115,6 +7174,16 @@ components:
artist:
$ref: "#/components/schemas/ArtistResult"

ResolvedRef:
type: object
description: An artist or album to open with `linkId` and `id`.
required: [linkId, id]
properties:
linkId:
type: string
id:
type: string

AlbumAbout:
type: object
required: [labels, genres]
Expand Down
23 changes: 23 additions & 0 deletions server/internal/artistpage/artistpage.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,29 @@ func Elsewhere(ctx context.Context, sess provider.Session, caps provider.Capabil
return nil
}

// AlbumElsewhere finds an album by title on another link, by the same
// artist when artist isn't empty, or nil.
func AlbumElsewhere(ctx context.Context, sess provider.Session, caps provider.Capabilities, title, artist string) *provider.Album {
if !caps.CanSearch(provider.KindAlbum) {
return nil
}
page, err := sess.Search(ctx, provider.SearchQuery{Text: title, Kinds: []provider.EntityKind{provider.KindAlbum}, Limit: 10})
if err != nil {
slog.Debug("artistpage: looking for an album elsewhere", "album", title, "err", err)
return nil
}
want, by := match.Simplify(title), match.Simplify(artist)
for _, a := range page.Albums {
if match.Simplify(a.Title) != want {
continue
}
if by == "" || slices.ContainsFunc(a.Artists, func(c provider.ArtistCredit) bool { return match.Simplify(c.Name) == by }) {
return &a
}
}
return nil
}

// each runs f for 0..n-1, a few at a time.
func each(n int, f func(i int)) {
sem := make(chan struct{}, parallel)
Expand Down
13 changes: 13 additions & 0 deletions server/internal/artistpage/artistpage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,5 +214,18 @@ func TestElsewhere(t *testing.T) {
}
}

func TestAlbumElsewhere(t *testing.T) {
sess, caps := open(t, fake.Options{})
if a := artistpage.AlbumElsewhere(t.Context(), sess, caps, "zero zero", "Null Island"); a == nil || a.Title != "Zero Zero" {
t.Errorf("AlbumElsewhere = %+v", a)
}
if a := artistpage.AlbumElsewhere(t.Context(), sess, caps, "A440", ""); a == nil || a.Title != "A440" {
t.Errorf("without an artist = %+v", a)
}
if a := artistpage.AlbumElsewhere(t.Context(), sess, caps, "Zero Zero", "Sine Language"); a != nil {
t.Errorf("another artist's album found %+v", a)
}
}

// seeded is the same "random" order every run.
func seeded() *rand.Rand { return rand.New(rand.NewPCG(1, 2)) } //nolint:gosec // a test's fixed order
Loading
Loading