Extends the watchlist export to the full library. The exporter is now general (core/exports/artist_export.py, renamed from watchlist_export) — adds tidal/qobuz links and an extra_fields passthrough, so the library export also carries lastfm/genius URLs + soul_id, and an optional "library counts" toggle adds owned album/track counts per artist. - GET /api/library/artists/export?format=&links=&contents= — pulls every artists row, normalizes onto the canonical *_artist_id keys, optionally GROUP-BY counts for album/track totals. - The export modal is now openArtistExportModal(scope): "Export Library" button in the library header + the existing "Export" on the watchlist bar (a thin wrapper). Library mode shows the extra "library counts" toggle. Tests (11): builder across formats + the new tidal/qobuz links + extra_fields columns; watchlist + library endpoint wiring. 64 integrity green; ruff clean.
109 lines
4.2 KiB
Python
109 lines
4.2 KiB
Python
"""Export an artist roster — watchlist OR library — to JSON / CSV / plain text
|
|
(corruption's request).
|
|
|
|
Pure shaping + formatting so it's the single source of truth and unit-testable —
|
|
web_server fetches the artists (normalizing each source's fields onto the canonical
|
|
``*_artist_id`` keys below) and hands them here; the UI just picks options and
|
|
downloads. Always exports the name + whatever source IDs each artist has;
|
|
``include_links`` adds external discography URLs; ``extra_fields`` passes through
|
|
source-specific extras (e.g. library album/track counts) in a stable order.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import io
|
|
import json
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
# Canonical id field → external URL builder.
|
|
_LINKS = {
|
|
'spotify_artist_id': lambda i: f'https://open.spotify.com/artist/{i}',
|
|
'musicbrainz_artist_id': lambda i: f'https://musicbrainz.org/artist/{i}',
|
|
'deezer_artist_id': lambda i: f'https://www.deezer.com/artist/{i}',
|
|
'discogs_artist_id': lambda i: f'https://www.discogs.com/artist/{i}',
|
|
'itunes_artist_id': lambda i: f'https://music.apple.com/artist/{i}',
|
|
'tidal_artist_id': lambda i: f'https://tidal.com/artist/{i}',
|
|
'qobuz_artist_id': lambda i: f'https://www.qobuz.com/artist/{i}',
|
|
}
|
|
# Stable order so CSV columns + JSON keys are deterministic. amazon carries an id
|
|
# but no clean public URL.
|
|
_ID_FIELDS = ['spotify_artist_id', 'musicbrainz_artist_id', 'deezer_artist_id',
|
|
'discogs_artist_id', 'itunes_artist_id', 'tidal_artist_id',
|
|
'qobuz_artist_id', 'amazon_artist_id']
|
|
|
|
VALID_FORMATS = ('json', 'csv', 'txt')
|
|
|
|
|
|
def _name(a: Dict[str, Any]) -> str:
|
|
return str(a.get('artist_name') or a.get('name') or '').strip()
|
|
|
|
|
|
def _short(field: str) -> str:
|
|
return field.replace('_artist_id', '')
|
|
|
|
|
|
def _row(a: Dict[str, Any], include_links: bool, extra_fields: List[str]) -> Dict[str, Any]:
|
|
row: Dict[str, Any] = {'name': _name(a)}
|
|
for f in _ID_FIELDS:
|
|
if a.get(f):
|
|
row[f] = str(a[f])
|
|
for f in extra_fields:
|
|
if a.get(f) not in (None, ''):
|
|
row[f] = a[f]
|
|
if include_links:
|
|
links = {_short(f): b(a[f]) for f, b in _LINKS.items() if a.get(f)}
|
|
if links:
|
|
row['links'] = links
|
|
return row
|
|
|
|
|
|
def build_artist_export(artists: Optional[List[Dict[str, Any]]],
|
|
fmt: str = 'json', include_links: bool = False,
|
|
extra_fields: Optional[List[str]] = None) -> str:
|
|
"""Return the roster serialized in ``fmt`` (json | csv | txt).
|
|
|
|
- ``txt`` → one artist name per line.
|
|
- ``csv`` → name + each source-id column + ``extra_fields`` columns (+ a
|
|
*_url column per service when ``include_links``).
|
|
- ``json`` → a list of objects: name, present source ids, present extras, and
|
|
a ``links`` map when ``include_links``.
|
|
"""
|
|
artists = artists or []
|
|
extra_fields = list(extra_fields or [])
|
|
fmt = (fmt or 'json').lower()
|
|
if fmt not in VALID_FORMATS:
|
|
fmt = 'json'
|
|
|
|
if fmt == 'txt':
|
|
return '\n'.join(n for n in (_name(a) for a in artists) if n)
|
|
|
|
if fmt == 'csv':
|
|
cols = ['name'] + _ID_FIELDS + extra_fields
|
|
if include_links:
|
|
cols += [f'{_short(f)}_url' for f in _LINKS]
|
|
out = io.StringIO()
|
|
w = csv.writer(out)
|
|
w.writerow(cols)
|
|
for a in artists:
|
|
line = [_name(a)] + [str(a.get(f) or '') for f in _ID_FIELDS]
|
|
line += [str(a.get(f) if a.get(f) is not None else '') for f in extra_fields]
|
|
if include_links:
|
|
line += [_LINKS[f](a[f]) if a.get(f) else '' for f in _LINKS]
|
|
w.writerow(line)
|
|
return out.getvalue()
|
|
|
|
return json.dumps([_row(a, include_links, extra_fields) for a in artists],
|
|
indent=2, ensure_ascii=False)
|
|
|
|
|
|
def export_mime_and_ext(fmt: str):
|
|
"""(content-type, file extension) for a format."""
|
|
return {
|
|
'json': ('application/json', 'json'),
|
|
'csv': ('text/csv', 'csv'),
|
|
'txt': ('text/plain', 'txt'),
|
|
}.get((fmt or 'json').lower(), ('application/json', 'json'))
|
|
|
|
|
|
__all__ = ['build_artist_export', 'export_mime_and_ext', 'VALID_FORMATS']
|