Search: extend pasted-link resolution to Discogs (#813)
#775 already resolves pasted Spotify / Apple / MusicBrainz / Deezer links to an exact artist/album/track on the Search page. Added Discogs to that set (the one source in the request not already covered; Amazon left out per request). - by_id resolver: discogs in SUPPORTED_SOURCES + _KNOWN_HOSTS; parse /artist/<id>-slug, /release/<id>-slug, /master/<id>-slug (master→album), and strip the slug to the leading numeric id. Discogs has no standalone track URLs (tracks live inside a release), so artist + album resolve. - Fetch dispatch: discogs albums use get_album(include_tracks=False) like itunes/musicbrainz; artists use get_artist; both already return the common normalized card shape. Updated the not-a-link hint to mention Discogs. Frontend needs no change — it adopts whatever source the resolver returns and Discogs is already a search source. Tests: parse release/master/artist (slug-stripped, scheme-less) + resolve release→get_album(numeric id) and artist→get_artist. 43 by_id tests pass.
This commit is contained in:
parent
df898b5212
commit
b2de64e87d
2 changed files with 69 additions and 6 deletions
|
|
@ -33,6 +33,7 @@ an injected ``client_resolver`` (defaulting to the orchestrator's
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Callable, NamedTuple, Optional
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
|
|
@ -42,13 +43,13 @@ logger = logging.getLogger(__name__)
|
|||
# providers whose public links a user would paste AND whose get-by-id returns
|
||||
# the common Spotify-shaped dict. Streaming download backends (Tidal/Qobuz)
|
||||
# return raw API shapes and aren't metadata-link sources, so they're omitted.
|
||||
SUPPORTED_SOURCES = ('spotify', 'itunes', 'musicbrainz', 'deezer')
|
||||
SUPPORTED_SOURCES = ('spotify', 'itunes', 'musicbrainz', 'deezer', 'discogs')
|
||||
|
||||
# Domains we recognize — used to detect a pasted URL even when the user
|
||||
# omitted the scheme (e.g. "open.spotify.com/album/…").
|
||||
_KNOWN_HOSTS = (
|
||||
'open.spotify.com', 'music.apple.com', 'itunes.apple.com',
|
||||
'musicbrainz.org', 'deezer.com',
|
||||
'musicbrainz.org', 'deezer.com', 'discogs.com',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -72,7 +73,7 @@ class LookupTarget(NamedTuple):
|
|||
|
||||
def _kind_from_keyword(keyword: str) -> Optional[str]:
|
||||
"""Map a URL/URI path keyword to a lookup kind."""
|
||||
if keyword in ('album', 'release', 'release-group'):
|
||||
if keyword in ('album', 'release', 'release-group', 'master'):
|
||||
return 'album'
|
||||
if keyword in ('track', 'recording', 'song'):
|
||||
return 'track'
|
||||
|
|
@ -130,6 +131,18 @@ def _parse_url(raw: str) -> list[LookupTarget]:
|
|||
# redirect; only handle canonical /album/ /track/ paths.
|
||||
return _by_keyword('deezer')
|
||||
|
||||
if 'discogs.com' in host:
|
||||
# Discogs paths are /artist/<id>-Slug, /release/<id>-Slug,
|
||||
# /master/<id>-Slug — the id is embedded with a slug, so strip to the
|
||||
# leading number. (Discogs has no standalone track URLs; tracks live
|
||||
# inside a release, so only artist/album resolve.)
|
||||
out = []
|
||||
for t in _by_keyword('discogs'):
|
||||
m = re.match(r'(\d+)', t.id)
|
||||
if m:
|
||||
out.append(t._replace(id=m.group(1)))
|
||||
return out
|
||||
|
||||
return []
|
||||
|
||||
|
||||
|
|
@ -254,7 +267,7 @@ def _fetch_album(client: Any, source: str, identifier: str) -> Optional[dict]:
|
|||
(the modal re-fetches the full tracklist on open)."""
|
||||
if source == 'deezer':
|
||||
return client.get_album_metadata(identifier, include_tracks=False)
|
||||
if source in ('itunes', 'musicbrainz'):
|
||||
if source in ('itunes', 'musicbrainz', 'discogs'):
|
||||
return client.get_album(identifier, include_tracks=False)
|
||||
return client.get_album(identifier) # spotify
|
||||
|
||||
|
|
@ -274,8 +287,8 @@ def _fetch_artist(client: Any, source: str, identifier: str) -> Optional[dict]:
|
|||
|
||||
# Shown in the dropdown's empty state so the user knows what to do next.
|
||||
_MSG_NOT_A_LINK = (
|
||||
'Paste a full link from Spotify, Apple Music, MusicBrainz, or Deezer '
|
||||
'(a bare ID is ambiguous).'
|
||||
'Paste a full link from Spotify, Apple Music, Deezer, Discogs, or '
|
||||
'MusicBrainz (a bare ID is ambiguous).'
|
||||
)
|
||||
_MSG_NOT_FOUND = "Couldn't resolve that link — double-check it's correct."
|
||||
|
||||
|
|
|
|||
|
|
@ -449,3 +449,53 @@ def test_resolve_get_album_returning_none_yields_not_found():
|
|||
)
|
||||
assert res['available'] is False
|
||||
assert res['albums'] == []
|
||||
|
||||
|
||||
# ── Discogs (#813 — extend paste-link to Discogs) ──────────────────────────
|
||||
|
||||
def test_parse_discogs_release_url_strips_slug():
|
||||
out = by_id.parse_metadata_identifier(
|
||||
'https://www.discogs.com/release/678910-Some-Album-Title')
|
||||
assert out == [by_id.LookupTarget('discogs', 'album', '678910')]
|
||||
|
||||
|
||||
def test_parse_discogs_master_url_is_album():
|
||||
out = by_id.parse_metadata_identifier(
|
||||
'https://www.discogs.com/master/555-A-Master')
|
||||
assert out == [by_id.LookupTarget('discogs', 'album', '555')]
|
||||
|
||||
|
||||
def test_parse_discogs_artist_url():
|
||||
out = by_id.parse_metadata_identifier(
|
||||
'https://www.discogs.com/artist/12345-Some-Artist')
|
||||
assert out == [by_id.LookupTarget('discogs', 'artist', '12345')]
|
||||
|
||||
|
||||
def test_parse_discogs_url_no_scheme():
|
||||
out = by_id.parse_metadata_identifier('discogs.com/release/999-X')
|
||||
assert out == [by_id.LookupTarget('discogs', 'album', '999')]
|
||||
|
||||
|
||||
def test_resolve_discogs_release_uses_get_album_with_numeric_id():
|
||||
client = _FakeClient(album={'id': '678910', 'name': 'Some Album',
|
||||
'artists': [{'name': 'Some Artist'}]})
|
||||
res = by_id.resolve_identifier(
|
||||
'https://www.discogs.com/release/678910-Some-Album-Title', deps=None,
|
||||
client_resolver=_resolver_from({'discogs': client}))
|
||||
assert res['available'] is True and res['source'] == 'discogs'
|
||||
assert res['albums'] and res['albums'][0]['name'] == 'Some Album'
|
||||
assert client.album_calls == ['678910'] # numeric id, slug stripped
|
||||
|
||||
|
||||
def test_resolve_discogs_artist():
|
||||
client = _FakeClient(artist={'id': '12345', 'name': 'Some Artist'})
|
||||
res = by_id.resolve_identifier(
|
||||
'https://www.discogs.com/artist/12345-Some-Artist', deps=None,
|
||||
client_resolver=_resolver_from({'discogs': client}))
|
||||
assert res['available'] is True and res['source'] == 'discogs'
|
||||
assert res['artists'] and res['artists'][0]['name'] == 'Some Artist'
|
||||
assert client.artist_calls == ['12345']
|
||||
|
||||
|
||||
def test_discogs_in_supported_sources():
|
||||
assert 'discogs' in by_id.SUPPORTED_SOURCES
|
||||
|
|
|
|||
Loading…
Reference in a new issue