mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-07-27 23:42:28 +02:00
feat: add configurable artist separator
This commit is contained in:
@@ -155,6 +155,8 @@ saved_max_width = -1
|
|||||||
set_playlist_to_album = true
|
set_playlist_to_album = true
|
||||||
# Use playlist position as tracknumber for playlist items
|
# Use playlist position as tracknumber for playlist items
|
||||||
renumber_playlist_tracks = true
|
renumber_playlist_tracks = true
|
||||||
|
# Separator used when a provider exposes multiple artists as separate values
|
||||||
|
artist_separator = "; "
|
||||||
# Metadata fields to exclude from tagging
|
# Metadata fields to exclude from tagging
|
||||||
exclude = []
|
exclude = []
|
||||||
|
|
||||||
|
|||||||
@@ -177,12 +177,8 @@ func toTags(meta Metadata) map[string]string {
|
|||||||
"source_artist_id": meta.SourceArtistID,
|
"source_artist_id": meta.SourceArtistID,
|
||||||
}
|
}
|
||||||
if meta.TrackNumber > 0 {
|
if meta.TrackNumber > 0 {
|
||||||
if meta.TrackTotal > 0 {
|
|
||||||
tags["track"] = fmt.Sprintf("%02d/%02d", meta.TrackNumber, meta.TrackTotal)
|
|
||||||
} else {
|
|
||||||
tags["track"] = fmt.Sprintf("%02d", meta.TrackNumber)
|
tags["track"] = fmt.Sprintf("%02d", meta.TrackNumber)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if meta.TrackTotal > 0 {
|
if meta.TrackTotal > 0 {
|
||||||
tags["tracktotal"] = strconv.Itoa(meta.TrackTotal)
|
tags["tracktotal"] = strconv.Itoa(meta.TrackTotal)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func TestToTagsTotalsAndSourceFields(t *testing.T) {
|
|||||||
SourcePlatform: "qobuz",
|
SourcePlatform: "qobuz",
|
||||||
SourceTrackID: "t1",
|
SourceTrackID: "t1",
|
||||||
})
|
})
|
||||||
if tags["track"] != "03/12" {
|
if tags["track"] != "03" {
|
||||||
t.Fatalf("track tag = %q", tags["track"])
|
t.Fatalf("track tag = %q", tags["track"])
|
||||||
}
|
}
|
||||||
if tags["disc"] != "1/2" {
|
if tags["disc"] != "1/2" {
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ type ArtworkConfig struct {
|
|||||||
type MetadataConfig struct {
|
type MetadataConfig struct {
|
||||||
SetPlaylistToAlbum bool `toml:"set_playlist_to_album"`
|
SetPlaylistToAlbum bool `toml:"set_playlist_to_album"`
|
||||||
RenumberPlaylistTracks bool `toml:"renumber_playlist_tracks"`
|
RenumberPlaylistTracks bool `toml:"renumber_playlist_tracks"`
|
||||||
|
ArtistSeparator string `toml:"artist_separator"`
|
||||||
Exclude []string `toml:"exclude"`
|
Exclude []string `toml:"exclude"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +298,7 @@ func DefaultConfigData() ConfigData {
|
|||||||
Metadata: MetadataConfig{
|
Metadata: MetadataConfig{
|
||||||
SetPlaylistToAlbum: true,
|
SetPlaylistToAlbum: true,
|
||||||
RenumberPlaylistTracks: true,
|
RenumberPlaylistTracks: true,
|
||||||
|
ArtistSeparator: "; ",
|
||||||
Exclude: []string{},
|
Exclude: []string{},
|
||||||
},
|
},
|
||||||
Filepaths: FilepathsConfig{
|
Filepaths: FilepathsConfig{
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return collectionMetadata(label, tracks), nil
|
return c.collectionMetadata(label, tracks), nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported beatport media type %q", mediaType)
|
return nil, fmt.Errorf("unsupported beatport media type %q", mediaType)
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ func (c *Client) getPlaylistMetadata(ctx context.Context, id string) (map[string
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if track, ok := m["track"].(map[string]any); ok {
|
if track, ok := m["track"].(map[string]any); ok {
|
||||||
tracks = append(tracks, normalizeTrackListItem(track))
|
tracks = append(tracks, c.normalizeTrackListItem(track))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return playlistMetadata(playlist, tracks), nil
|
return playlistMetadata(playlist, tracks), nil
|
||||||
@@ -231,7 +231,7 @@ func (c *Client) getChartMetadata(ctx context.Context, id string) (map[string]an
|
|||||||
normalized := make([]any, 0, len(tracks))
|
normalized := make([]any, 0, len(tracks))
|
||||||
for _, raw := range tracks {
|
for _, raw := range tracks {
|
||||||
if track, ok := raw.(map[string]any); ok {
|
if track, ok := raw.(map[string]any); ok {
|
||||||
normalized = append(normalized, normalizeTrackListItem(track))
|
normalized = append(normalized, c.normalizeTrackListItem(track))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return playlistMetadata(chart, normalized), nil
|
return playlistMetadata(chart, normalized), nil
|
||||||
@@ -507,7 +507,7 @@ func (c *Client) normalizeRelease(raw map[string]any, tracks []any) map[string]a
|
|||||||
|
|
||||||
func (c *Client) normalizeTrack(raw map[string]any) map[string]any {
|
func (c *Client) normalizeTrack(raw map[string]any) map[string]any {
|
||||||
release := mapAny(raw["release"])
|
release := mapAny(raw["release"])
|
||||||
artistName := joinArtists(raw["artists"])
|
artistName := c.joinArtists(raw["artists"])
|
||||||
artistID := firstArtistID(raw["artists"])
|
artistID := firstArtistID(raw["artists"])
|
||||||
albumArtist := c.releaseArtistName(release["artists"])
|
albumArtist := c.releaseArtistName(release["artists"])
|
||||||
if albumArtist == "" {
|
if albumArtist == "" {
|
||||||
@@ -543,25 +543,25 @@ func (c *Client) normalizeTrack(raw map[string]any) map[string]any {
|
|||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeTrackListItem(raw map[string]any) map[string]any {
|
func (c *Client) normalizeTrackListItem(raw map[string]any) map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"id": jsonutil.StringFromAny(raw["id"]),
|
"id": jsonutil.StringFromAny(raw["id"]),
|
||||||
"title": jsonutil.StringFromAny(raw["name"]),
|
"title": jsonutil.StringFromAny(raw["name"]),
|
||||||
"version": jsonutil.StringFromAny(raw["mix_name"]),
|
"version": jsonutil.StringFromAny(raw["mix_name"]),
|
||||||
"artist": map[string]any{"name": joinArtists(raw["artists"])},
|
"artist": map[string]any{"name": c.joinArtists(raw["artists"])},
|
||||||
"album": map[string]any{"id": jsonutil.NestedString(raw, "release", "id"), "title": jsonutil.NestedString(raw, "release", "name")},
|
"album": map[string]any{"id": jsonutil.NestedString(raw, "release", "id"), "title": jsonutil.NestedString(raw, "release", "name")},
|
||||||
"track_number": jsonutil.IntFromAny(raw["number"]),
|
"track_number": jsonutil.IntFromAny(raw["number"]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionMetadata(raw map[string]any, tracks []any) map[string]any {
|
func (c *Client) collectionMetadata(raw map[string]any, tracks []any) map[string]any {
|
||||||
items := make([]any, 0, len(tracks))
|
items := make([]any, 0, len(tracks))
|
||||||
for _, entry := range tracks {
|
for _, entry := range tracks {
|
||||||
track, ok := entry.(map[string]any)
|
track, ok := entry.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
items = append(items, normalizeTrackListItem(track))
|
items = append(items, c.normalizeTrackListItem(track))
|
||||||
}
|
}
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"id": jsonutil.StringFromAny(raw["id"]),
|
"id": jsonutil.StringFromAny(raw["id"]),
|
||||||
@@ -620,7 +620,7 @@ func (c *Client) normalizeSearchItems(mediaType string, items []any) []any {
|
|||||||
}
|
}
|
||||||
switch mediaType {
|
switch mediaType {
|
||||||
case "track":
|
case "track":
|
||||||
out = append(out, normalizeTrackListItem(m))
|
out = append(out, c.normalizeTrackListItem(m))
|
||||||
case "album":
|
case "album":
|
||||||
out = append(out, map[string]any{"id": jsonutil.StringFromAny(m["id"]), "title": jsonutil.StringFromAny(m["name"]), "artist": map[string]any{"name": c.releaseArtistName(m["artists"])}, "tracks_count": jsonutil.IntFromAny(m["track_count"]), "release_date_original": jsonutil.StringFromAny(m["new_release_date"])})
|
out = append(out, map[string]any{"id": jsonutil.StringFromAny(m["id"]), "title": jsonutil.StringFromAny(m["name"]), "artist": map[string]any{"name": c.releaseArtistName(m["artists"])}, "tracks_count": jsonutil.IntFromAny(m["track_count"]), "release_date_original": jsonutil.StringFromAny(m["new_release_date"])})
|
||||||
case "label":
|
case "label":
|
||||||
@@ -653,7 +653,7 @@ func audioProfile(quality, streamQuality string) (provider.AudioProfile, string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinArtists(v any) string {
|
func (c *Client) joinArtists(v any) string {
|
||||||
items := sliceAny(v)
|
items := sliceAny(v)
|
||||||
names := make([]string, 0, len(items))
|
names := make([]string, 0, len(items))
|
||||||
for _, raw := range items {
|
for _, raw := range items {
|
||||||
@@ -665,7 +665,7 @@ func joinArtists(v any) string {
|
|||||||
names = append(names, name)
|
names = append(names, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return strings.Join(names, ", ")
|
return strings.Join(names, artistSeparator(c.cfg.Session.Metadata.ArtistSeparator))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) releaseArtistName(v any) string {
|
func (c *Client) releaseArtistName(v any) string {
|
||||||
@@ -674,7 +674,14 @@ func (c *Client) releaseArtistName(v any) string {
|
|||||||
if threshold >= 0 && len(items) > threshold {
|
if threshold >= 0 && len(items) > threshold {
|
||||||
return "Various Artists"
|
return "Various Artists"
|
||||||
}
|
}
|
||||||
return joinArtists(v)
|
return c.joinArtists(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func artistSeparator(separator string) string {
|
||||||
|
if separator == "" {
|
||||||
|
return "; "
|
||||||
|
}
|
||||||
|
return separator
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstArtistID(v any) string {
|
func firstArtistID(v any) string {
|
||||||
|
|||||||
@@ -164,11 +164,24 @@ func TestReleaseArtistCollapseCanBeDisabled(t *testing.T) {
|
|||||||
map[string]any{"name": "C"},
|
map[string]any{"name": "C"},
|
||||||
map[string]any{"name": "D"},
|
map[string]any{"name": "D"},
|
||||||
})
|
})
|
||||||
if got != "A, B, C, D" {
|
if got != "A; B; C; D" {
|
||||||
t.Fatalf("releaseArtistName() = %q", got)
|
t.Fatalf("releaseArtistName() = %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestArtistSeparatorCanBeConfigured(t *testing.T) {
|
||||||
|
cfg := &config.Config{Session: config.DefaultConfigData(), File: config.DefaultConfigData()}
|
||||||
|
cfg.Session.Metadata.ArtistSeparator = " / "
|
||||||
|
c := New(cfg)
|
||||||
|
got := c.joinArtists([]any{
|
||||||
|
map[string]any{"name": "A"},
|
||||||
|
map[string]any{"name": "B"},
|
||||||
|
})
|
||||||
|
if got != "A / B" {
|
||||||
|
t.Fatalf("joinArtists() = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestArtistMetadataUsesReleases(t *testing.T) {
|
func TestArtistMetadataUsesReleases(t *testing.T) {
|
||||||
c, closeServer := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
c, closeServer := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ func (c *Client) getTrackMetadata(ctx context.Context, item string) (map[string]
|
|||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil, errors.New("yandex track metadata missing result")
|
return nil, errors.New("yandex track metadata missing result")
|
||||||
}
|
}
|
||||||
return normalizeTrack(items[0], trackRef), nil
|
return normalizeTrack(items[0], trackRef, c.artistSeparator()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getAlbumMetadata(ctx context.Context, item string) (map[string]any, error) {
|
func (c *Client) getAlbumMetadata(ctx context.Context, item string) (map[string]any, error) {
|
||||||
@@ -198,7 +198,7 @@ func (c *Client) getAlbumMetadata(ctx context.Context, item string) (map[string]
|
|||||||
if len(result) == 0 {
|
if len(result) == 0 {
|
||||||
return nil, errors.New("yandex album metadata missing result")
|
return nil, errors.New("yandex album metadata missing result")
|
||||||
}
|
}
|
||||||
return normalizeAlbum(result), nil
|
return normalizeAlbum(result, c.artistSeparator()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getArtistMetadata(ctx context.Context, item string) (map[string]any, error) {
|
func (c *Client) getArtistMetadata(ctx context.Context, item string) (map[string]any, error) {
|
||||||
@@ -273,7 +273,7 @@ func (c *Client) normalizeSearchItems(mediaType string, result map[string]any) [
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
appendItem(normalizeTrack(itm, canonicalTrackRefFromRaw(itm, "")))
|
appendItem(normalizeTrack(itm, canonicalTrackRefFromRaw(itm, ""), c.artistSeparator()))
|
||||||
}
|
}
|
||||||
case "album":
|
case "album":
|
||||||
for _, raw := range getResults("albums") {
|
for _, raw := range getResults("albums") {
|
||||||
@@ -281,7 +281,7 @@ func (c *Client) normalizeSearchItems(mediaType string, result map[string]any) [
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
appendItem(normalizeAlbumSearchItem(itm))
|
appendItem(normalizeAlbumSearchItem(itm, c.artistSeparator()))
|
||||||
}
|
}
|
||||||
case "artist":
|
case "artist":
|
||||||
for _, raw := range getResults("artists") {
|
for _, raw := range getResults("artists") {
|
||||||
@@ -561,11 +561,11 @@ func resultMap(resp map[string]any) map[string]any {
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeTrack(raw map[string]any, fallbackID string) map[string]any {
|
func normalizeTrack(raw map[string]any, fallbackID string, artistSeparator string) map[string]any {
|
||||||
trackID := canonicalTrackRefFromRaw(raw, fallbackID)
|
trackID := canonicalTrackRefFromRaw(raw, fallbackID)
|
||||||
albumRaw := firstAlbum(raw)
|
albumRaw := firstAlbum(raw)
|
||||||
artistName := joinArtists(raw)
|
artistName := joinArtists(raw, artistSeparator)
|
||||||
albumArtist := joinAlbumArtists(albumRaw)
|
albumArtist := joinAlbumArtists(albumRaw, artistSeparator)
|
||||||
if albumArtist == "" {
|
if albumArtist == "" {
|
||||||
albumArtist = artistName
|
albumArtist = artistName
|
||||||
}
|
}
|
||||||
@@ -599,10 +599,10 @@ func normalizeTrack(raw map[string]any, fallbackID string) map[string]any {
|
|||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeAlbum(raw map[string]any) map[string]any {
|
func normalizeAlbum(raw map[string]any, artistSeparator string) map[string]any {
|
||||||
artistName := joinArtists(raw)
|
artistName := joinArtists(raw, artistSeparator)
|
||||||
if artistName == "" {
|
if artistName == "" {
|
||||||
artistName = joinAlbumArtists(raw)
|
artistName = joinAlbumArtists(raw, artistSeparator)
|
||||||
}
|
}
|
||||||
volumes := albumVolumes(raw)
|
volumes := albumVolumes(raw)
|
||||||
items := make([]any, 0)
|
items := make([]any, 0)
|
||||||
@@ -698,8 +698,8 @@ func normalizeAlbumSummary(albumRaw map[string]any, artistName string) map[strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeAlbumSearchItem(raw map[string]any) map[string]any {
|
func normalizeAlbumSearchItem(raw map[string]any, artistSeparator string) map[string]any {
|
||||||
artistName := joinArtists(raw)
|
artistName := joinArtists(raw, artistSeparator)
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"id": jsonutil.StringFromAny(raw["id"]),
|
"id": jsonutil.StringFromAny(raw["id"]),
|
||||||
"title": jsonutil.StringFromAny(raw["title"]),
|
"title": jsonutil.StringFromAny(raw["title"]),
|
||||||
@@ -768,7 +768,7 @@ func firstArtistID(raw map[string]any) string {
|
|||||||
return jsonutil.StringFromAny(artist["id"])
|
return jsonutil.StringFromAny(artist["id"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinArtists(raw map[string]any) string {
|
func joinArtists(raw map[string]any, artistSeparator string) string {
|
||||||
artists, _ := raw["artists"].([]any)
|
artists, _ := raw["artists"].([]any)
|
||||||
parts := make([]string, 0, len(artists))
|
parts := make([]string, 0, len(artists))
|
||||||
for _, entry := range artists {
|
for _, entry := range artists {
|
||||||
@@ -780,10 +780,10 @@ func joinArtists(raw map[string]any) string {
|
|||||||
parts = append(parts, name)
|
parts = append(parts, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return strings.Join(parts, ", ")
|
return strings.Join(parts, artistSeparator)
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinAlbumArtists(albumRaw map[string]any) string {
|
func joinAlbumArtists(albumRaw map[string]any, artistSeparator string) string {
|
||||||
if len(albumRaw) == 0 {
|
if len(albumRaw) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -798,7 +798,14 @@ func joinAlbumArtists(albumRaw map[string]any) string {
|
|||||||
parts = append(parts, name)
|
parts = append(parts, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return strings.Join(parts, ", ")
|
return strings.Join(parts, artistSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) artistSeparator() string {
|
||||||
|
if c.cfg.Session.Metadata.ArtistSeparator == "" {
|
||||||
|
return "; "
|
||||||
|
}
|
||||||
|
return c.cfg.Session.Metadata.ArtistSeparator
|
||||||
}
|
}
|
||||||
|
|
||||||
func trackNumbers(albumRaw map[string]any) (int, int) {
|
func trackNumbers(albumRaw map[string]any) (int, int) {
|
||||||
|
|||||||
@@ -115,13 +115,13 @@ func TestGetMetadataTrackUsesModernTracksEndpoint(t *testing.T) {
|
|||||||
"id": "9442712",
|
"id": "9442712",
|
||||||
"realId": "9442712",
|
"realId": "9442712",
|
||||||
"title": "Nightcall",
|
"title": "Nightcall",
|
||||||
"artists": []map[string]any{{"id": "1433871", "name": "Kavinsky"}},
|
"artists": []map[string]any{{"id": "1433871", "name": "Kavinsky"}, {"id": "42", "name": "Lovefoxxx"}},
|
||||||
"albums": []map[string]any{{
|
"albums": []map[string]any{{
|
||||||
"id": "1000856",
|
"id": "1000856",
|
||||||
"title": "OutRun",
|
"title": "OutRun",
|
||||||
"releaseDate": "2013-02-25T00:00:00+04:00",
|
"releaseDate": "2013-02-25T00:00:00+04:00",
|
||||||
"trackCount": 13,
|
"trackCount": 13,
|
||||||
"artists": []map[string]any{{"id": "1433871", "name": "Kavinsky"}},
|
"artists": []map[string]any{{"id": "1433871", "name": "Kavinsky"}, {"id": "42", "name": "Lovefoxxx"}},
|
||||||
"trackPosition": map[string]any{
|
"trackPosition": map[string]any{
|
||||||
"index": 0,
|
"index": 0,
|
||||||
"volume": 1,
|
"volume": 1,
|
||||||
@@ -155,6 +155,12 @@ func TestGetMetadataTrackUsesModernTracksEndpoint(t *testing.T) {
|
|||||||
if album, _ := meta["album"].(map[string]any); jsonutil.StringFromAny(album["title"]) != "OutRun" {
|
if album, _ := meta["album"].(map[string]any); jsonutil.StringFromAny(album["title"]) != "OutRun" {
|
||||||
t.Fatalf("unexpected album: %+v", album)
|
t.Fatalf("unexpected album: %+v", album)
|
||||||
}
|
}
|
||||||
|
if artist := jsonutil.NestedString(meta, "artist", "name"); artist != "Kavinsky; Lovefoxxx" {
|
||||||
|
t.Fatalf("artist = %q", artist)
|
||||||
|
}
|
||||||
|
if albumArtist := jsonutil.NestedString(meta, "album", "artist", "name"); albumArtist != "Kavinsky; Lovefoxxx" {
|
||||||
|
t.Fatalf("album artist = %q", albumArtist)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLegacyDirectURLBuildsPlayableMP3URL(t *testing.T) {
|
func TestLegacyDirectURLBuildsPlayableMP3URL(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user