agent-discogs 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,794 @@
1
+ """Get command handler — dispatches by noun to entity-specific functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import Any
7
+
8
+ import click
9
+ from discogs_sdk import ArtistRelease, Discogs, LabelRelease, MasterVersion
10
+
11
+ from agent_discogs.client import get_client
12
+ from agent_discogs.errors import error_document, fail, format_error
13
+ from agent_discogs.formatting import (
14
+ format_artist,
15
+ format_artist_releases,
16
+ format_credits,
17
+ format_identifiers,
18
+ format_label,
19
+ format_label_releases,
20
+ format_master,
21
+ format_master_versions,
22
+ format_price_guide,
23
+ format_release,
24
+ format_tracklist,
25
+ )
26
+ from agent_discogs.json_output import Mode, dump, raw
27
+ from agent_discogs.pagination import (
28
+ DEFAULT_LIMIT,
29
+ fetch_filtered_page,
30
+ fetch_page,
31
+ next_page_cmd,
32
+ parse_cursor,
33
+ )
34
+ from agent_discogs.projections import (
35
+ drop_empty,
36
+ project_artist,
37
+ project_artist_release,
38
+ project_credits,
39
+ project_identifiers,
40
+ project_label,
41
+ project_label_release,
42
+ project_master,
43
+ project_master_version,
44
+ project_price,
45
+ project_release,
46
+ project_tracklist,
47
+ )
48
+ from agent_discogs.refs import make_ref, parse_ref
49
+
50
+ # Which entity type each noun expects
51
+ NOUN_EXPECTED_TYPE = {
52
+ "artist": "artist",
53
+ "credits": "release",
54
+ "identifiers": "release",
55
+ "label": "label",
56
+ "master": "master",
57
+ "price": "release",
58
+ "release": "release",
59
+ "releases": "artist",
60
+ "tracklist": "release",
61
+ "versions": "master",
62
+ }
63
+
64
+ NOUN_ALIASES = {"ids": "identifiers"}
65
+
66
+ GET_NOUNS = [*NOUN_EXPECTED_TYPE, *NOUN_ALIASES]
67
+
68
+
69
+ def _resolve_ref(ref_string: str, noun: str) -> tuple[str, int]:
70
+ """Parse a ref or raw ID, validating type against noun expectation.
71
+
72
+ Returns (entity_type, entity_id).
73
+ Raises ValueError with helpful message on mismatch.
74
+
75
+ When a raw numeric ID is given, parse_ref returns type "unknown".
76
+ We skip type validation in that case and trust the noun to determine
77
+ the entity type (e.g. "get release 367113" treats 367113 as a release).
78
+ This accommodates AI agents that pass raw Discogs IDs without our
79
+ typed-prefix convention.
80
+ """
81
+ entity_type, entity_id = parse_ref(ref_string)
82
+
83
+ expected = NOUN_EXPECTED_TYPE.get(noun)
84
+ if entity_type == "unknown" or expected is None:
85
+ return entity_type, entity_id
86
+
87
+ if entity_type != expected:
88
+ # Smart resolution: versions with a release ref
89
+ if noun == "versions" and entity_type == "release":
90
+ return entity_type, entity_id
91
+ # A label has a catalogue too: `get releases @l647`.
92
+ if noun == "releases" and entity_type == "label":
93
+ return entity_type, entity_id
94
+
95
+ def _a(word: str) -> str:
96
+ return f"an {word}" if word[0] in "aeiou" else f"a {word}"
97
+
98
+ raise ValueError(
99
+ f"{ref_string} is {_a(entity_type)}, not {_a(expected)}. "
100
+ f"Use {_a(expected)} ref or ID with 'get {noun}'."
101
+ )
102
+
103
+ return entity_type, entity_id
104
+
105
+
106
+ def _get_artist(client: Discogs, entity_id: int, mode: Mode) -> Any:
107
+ artist = client.artists.get(entity_id)
108
+ if mode.json:
109
+ return mode.entity(artist, project_artist)
110
+ return format_artist(artist)
111
+
112
+
113
+ def _get_credits(client: Discogs, entity_id: int, mode: Mode) -> Any:
114
+ release = client.releases.get(entity_id)
115
+ if mode.full:
116
+ return {"credits": [c.model_dump() for c in release.extra_artists or []]}
117
+ if mode.json:
118
+ return drop_empty(
119
+ {"ref": make_ref("release", entity_id), "credits": project_credits(release)}
120
+ )
121
+ return format_credits(release)
122
+
123
+
124
+ def _get_identifiers(client: Discogs, entity_id: int, mode: Mode) -> Any:
125
+ release = client.releases.get(entity_id)
126
+ if mode.full:
127
+ return {"identifiers": [i.model_dump() for i in release.identifiers or []]}
128
+ if mode.json:
129
+ return drop_empty(
130
+ {
131
+ "ref": make_ref("release", entity_id),
132
+ "identifiers": project_identifiers(release),
133
+ }
134
+ )
135
+ return format_identifiers(release)
136
+
137
+
138
+ def _get_label(client: Discogs, entity_id: int, mode: Mode) -> Any:
139
+ label = client.labels.get(entity_id)
140
+ if mode.json:
141
+ return mode.entity(label, project_label)
142
+ return format_label(label)
143
+
144
+
145
+ def _get_master(client: Discogs, entity_id: int, mode: Mode) -> Any:
146
+ master = client.masters.get(entity_id)
147
+ if mode.json:
148
+ return mode.entity(master, project_master)
149
+ return format_master(master)
150
+
151
+
152
+ def _get_price(client: Discogs, entity_id: int, mode: Mode) -> Any:
153
+ release = client.releases.get(entity_id)
154
+ price_suggestions = release.price_suggestions.get()
155
+ marketplace_stats = release.marketplace_stats.get()
156
+ if mode.full:
157
+ return {**raw(price_suggestions), "marketplace_stats": raw(marketplace_stats)}
158
+ if mode.json:
159
+ return project_price(release, price_suggestions, marketplace_stats)
160
+ return format_price_guide(release, price_suggestions, marketplace_stats)
161
+
162
+
163
+ def _get_release(
164
+ client: Discogs, entity_id: int, *, verbose: bool, compact: bool, mode: Mode
165
+ ) -> Any:
166
+ release = client.releases.get(entity_id)
167
+ if mode.json:
168
+ return mode.entity(
169
+ release, lambda r: project_release(r, verbose=verbose, compact=compact)
170
+ )
171
+ return format_release(release, verbose=verbose, compact=compact)
172
+
173
+
174
+ def _sort_params(sort: str | None, desc: bool) -> dict[str, str]:
175
+ """Server-side ordering; `--desc` only means something with `--sort`."""
176
+ if not sort:
177
+ return {}
178
+ return {"sort": sort, "sort_order": "desc" if desc else "asc"}
179
+
180
+
181
+ def _get_label_releases(
182
+ client: Discogs,
183
+ entity_id: int,
184
+ *,
185
+ page: int | None,
186
+ limit: int,
187
+ after: str | None,
188
+ year: str | None,
189
+ mode: Mode,
190
+ ) -> Any:
191
+ """A label's catalogue. The API has no filters or sorting here, so `--year`
192
+ is a client-side scan with cursor continuation (like `--role`)."""
193
+ label = client.labels.get(entity_id)
194
+ label_ref = make_ref("label", entity_id)
195
+ path = f"/labels/{entity_id}/releases"
196
+
197
+ if year:
198
+ result = fetch_filtered_page(
199
+ client,
200
+ path,
201
+ {},
202
+ LabelRelease,
203
+ "releases",
204
+ limit=limit,
205
+ keep=lambda item: str(getattr(item, "year", None) or "") == year,
206
+ cursor=after,
207
+ )
208
+ else:
209
+ result = fetch_page(
210
+ client,
211
+ path,
212
+ {"page": page or 1, "per_page": limit},
213
+ LabelRelease,
214
+ "releases",
215
+ )
216
+
217
+ if mode.json:
218
+ return mode.page(result, project_label_release)
219
+
220
+ footer_cmd = None
221
+ if result.has_next:
222
+ footer_cmd = next_page_cmd(
223
+ ["get", "releases", label_ref],
224
+ year=year,
225
+ limit=limit if limit != DEFAULT_LIMIT else None,
226
+ after=result.next_cursor,
227
+ page=None if result.filtered else result.page + 1,
228
+ )
229
+ return format_label_releases(
230
+ result.items,
231
+ label_ref,
232
+ label.name,
233
+ result.page,
234
+ result.total_items,
235
+ footer_cmd,
236
+ filtered=result.filtered,
237
+ capped=result.capped,
238
+ )
239
+
240
+
241
+ def _get_releases(
242
+ client: Discogs,
243
+ entity_type: str,
244
+ entity_id: int,
245
+ *,
246
+ page: int | None,
247
+ limit: int,
248
+ after: str | None,
249
+ role: str | None,
250
+ year: str | None,
251
+ sort: str | None,
252
+ desc: bool,
253
+ mode: Mode,
254
+ ) -> Any:
255
+ if entity_type == "label":
256
+ return _get_label_releases(
257
+ client,
258
+ entity_id,
259
+ page=page,
260
+ limit=limit,
261
+ after=after,
262
+ year=year,
263
+ mode=mode,
264
+ )
265
+
266
+ artist = client.artists.get(entity_id)
267
+ artist_name = artist.name
268
+ artist_ref = make_ref("artist", entity_id)
269
+
270
+ path = f"/artists/{entity_id}/releases"
271
+ params: dict[str, Any] = _sort_params(sort, desc)
272
+
273
+ # Flag validity is checked in _dispatch before any API call.
274
+ if role:
275
+ role_lower = role.lower()
276
+ result = fetch_filtered_page(
277
+ client,
278
+ path,
279
+ params,
280
+ ArtistRelease,
281
+ "releases",
282
+ limit=limit,
283
+ keep=lambda item: role_lower in (getattr(item, "role", "") or "").lower(),
284
+ cursor=after,
285
+ )
286
+ else:
287
+ params["page"] = page or 1
288
+ params["per_page"] = limit
289
+ result = fetch_page(client, path, params, ArtistRelease, "releases")
290
+
291
+ if mode.json:
292
+ return mode.page(result, project_artist_release)
293
+
294
+ footer_cmd = None
295
+ if result.has_next:
296
+ footer_cmd = next_page_cmd(
297
+ ["get", "releases", artist_ref],
298
+ role=role,
299
+ sort=sort,
300
+ desc=desc,
301
+ limit=limit if limit != DEFAULT_LIMIT else None,
302
+ after=result.next_cursor,
303
+ page=None if result.filtered else result.page + 1,
304
+ )
305
+
306
+ return format_artist_releases(
307
+ result.items,
308
+ artist_ref,
309
+ artist_name,
310
+ result.page,
311
+ result.total_items,
312
+ footer_cmd,
313
+ filtered=result.filtered,
314
+ capped=result.capped,
315
+ )
316
+
317
+
318
+ def _get_tracklist(client: Discogs, entity_id: int, mode: Mode) -> Any:
319
+ release = client.releases.get(entity_id)
320
+ if mode.full:
321
+ return {"tracklist": [t.model_dump() for t in release.tracklist or []]}
322
+ if mode.json:
323
+ return project_tracklist(release)
324
+ return format_tracklist(release)
325
+
326
+
327
+ def _get_versions(
328
+ client: Discogs,
329
+ entity_type: str,
330
+ entity_id: int,
331
+ ref_string: str,
332
+ *,
333
+ page: int | None,
334
+ limit: int,
335
+ country: str | None,
336
+ format: str | None, # noqa: A002 # click option name for --format
337
+ label: str | None,
338
+ year: str | None,
339
+ sort: str | None,
340
+ desc: bool,
341
+ mode: Mode,
342
+ ) -> Any:
343
+ master_id = entity_id
344
+ master_title = ""
345
+
346
+ # Smart resolution: release ref → look up its master_id
347
+ if entity_type == "release":
348
+ release = client.releases.get(entity_id)
349
+ if not release.master_id:
350
+ raise ValueError(
351
+ f"{ref_string} is a release with no master. "
352
+ "This release isn't linked to a master release. "
353
+ "Try searching for the master directly: "
354
+ f'agent-discogs search master "{release.title}"'
355
+ )
356
+ master_id = release.master_id
357
+ master_title = release.title
358
+ else:
359
+ master = client.masters.get(master_id)
360
+ master_title = master.title
361
+
362
+ master_ref = make_ref("master", master_id)
363
+
364
+ params: dict[str, Any] = {"page": page or 1, "per_page": limit}
365
+ if country:
366
+ params["country"] = country
367
+ if format:
368
+ params["format"] = format
369
+ if label:
370
+ params["label"] = label
371
+ if year:
372
+ params["released"] = year # the API calls the year filter `released`
373
+ params.update(_sort_params(sort, desc))
374
+
375
+ result = fetch_page(
376
+ client,
377
+ f"/masters/{master_id}/versions",
378
+ params,
379
+ MasterVersion,
380
+ "versions",
381
+ )
382
+
383
+ if mode.json:
384
+ return mode.page(result, project_master_version)
385
+
386
+ footer_cmd = None
387
+ if result.has_next:
388
+ footer_cmd = next_page_cmd(
389
+ ["get", "versions", master_ref],
390
+ country=country,
391
+ format=format,
392
+ label=label,
393
+ year=year,
394
+ sort=sort,
395
+ desc=desc,
396
+ limit=limit if limit != DEFAULT_LIMIT else None,
397
+ page=result.page + 1,
398
+ )
399
+
400
+ return format_master_versions(
401
+ result.items,
402
+ master_ref,
403
+ master_title,
404
+ result.page,
405
+ result.total_items,
406
+ footer_cmd,
407
+ )
408
+
409
+
410
+ SORT_KEYS: dict[str, tuple[str, ...]] = {
411
+ "releases": ("year", "title", "format"),
412
+ "versions": ("released", "title", "format", "label", "catno", "country"),
413
+ }
414
+
415
+
416
+ def _flag_error(
417
+ noun: str,
418
+ refs: tuple[str, ...],
419
+ *,
420
+ page: int | None,
421
+ after: str | None,
422
+ role: str | None,
423
+ year: str | None,
424
+ sort: str | None,
425
+ desc: bool,
426
+ ) -> str | None:
427
+ """Reject flag combinations that make no sense for this noun.
428
+
429
+ Checked before any API call so a bad flag never costs a request or gets
430
+ masked by an auth/network failure. Client-side filtered listings (which
431
+ page by cursor, not by number) are `releases --role` on an artist and
432
+ `releases --year` on a label.
433
+ """
434
+ label_refs = noun == "releases" and any(r.startswith("@l") for r in refs)
435
+ artist_refs = noun == "releases" and not all(r.startswith("@l") for r in refs)
436
+ filtered = noun == "releases" and (bool(role) or (label_refs and bool(year)))
437
+ if after is not None and not filtered:
438
+ return (
439
+ "--after only continues a client-side filtered listing "
440
+ "(get releases --role, or get releases @l... --year). This command "
441
+ "pages server-side or is not paginated; use --page where applicable."
442
+ )
443
+ if filtered and page is not None:
444
+ return (
445
+ "--page is not available while results are filtered client-side "
446
+ "(--role, or --year on a label catalogue). Use the Next page "
447
+ "command from the previous output."
448
+ )
449
+ if after is not None:
450
+ try:
451
+ parse_cursor(after)
452
+ except ValueError as exc:
453
+ return str(exc)
454
+ if year and noun not in ("versions", "releases"):
455
+ return "--year applies to 'get versions' and 'get releases @l...' only."
456
+ if year and artist_refs:
457
+ return (
458
+ "--year filters versions and label catalogues; for an artist "
459
+ "discography use --sort year (optionally --desc) and page."
460
+ )
461
+ if (sort or desc) and noun not in SORT_KEYS:
462
+ return "--sort/--desc apply to 'get releases' and 'get versions' only."
463
+ if desc and not sort:
464
+ return "--desc needs --sort <key>."
465
+ if sort and sort not in SORT_KEYS.get(noun, ()):
466
+ keys = ", ".join(SORT_KEYS[noun])
467
+ return f"--sort {sort!r} is not valid for 'get {noun}'. Keys: {keys}."
468
+ if label_refs and (role or sort):
469
+ return (
470
+ "Label catalogues cannot be role-filtered or sorted: the Discogs API "
471
+ "offers no --role or --sort for /labels/{id}/releases. Use --year "
472
+ "(client-side), page with --page, or search: "
473
+ "agent-discogs search release --label <name>."
474
+ )
475
+ return None
476
+
477
+
478
+ def _mode(json_output: bool, full: bool) -> Mode:
479
+ """`--full` is a JSON shape switch, so it needs `--json`; say so up front."""
480
+ if full and not json_output:
481
+ fail(ValueError("--full requires --json."), json_output=False)
482
+ return Mode(json=json_output, full=full)
483
+
484
+
485
+ MAX_REFS = 10
486
+
487
+
488
+ def _run_one(
489
+ client: Discogs,
490
+ noun: str,
491
+ ref: str,
492
+ *,
493
+ page: int | None,
494
+ after: str | None,
495
+ limit: int,
496
+ country: str | None,
497
+ format: str | None, # noqa: A002 # click option name for --format
498
+ label: str | None,
499
+ year: str | None,
500
+ sort: str | None,
501
+ desc: bool,
502
+ role: str | None,
503
+ verbose: bool,
504
+ compact: bool,
505
+ mode: Mode,
506
+ ) -> Any:
507
+ """One noun for one ref: text, or the JSON document."""
508
+ entity_type, entity_id = _resolve_ref(ref, noun)
509
+
510
+ if noun == "artist":
511
+ return _get_artist(client, entity_id, mode)
512
+ if noun == "credits":
513
+ return _get_credits(client, entity_id, mode)
514
+ if noun == "identifiers":
515
+ return _get_identifiers(client, entity_id, mode)
516
+ if noun == "master":
517
+ return _get_master(client, entity_id, mode)
518
+ if noun == "label":
519
+ return _get_label(client, entity_id, mode)
520
+ if noun == "price":
521
+ return _get_price(client, entity_id, mode)
522
+ if noun == "release":
523
+ return _get_release(
524
+ client, entity_id, verbose=verbose, compact=compact, mode=mode
525
+ )
526
+ if noun == "releases":
527
+ return _get_releases(
528
+ client,
529
+ entity_type,
530
+ entity_id,
531
+ page=page,
532
+ after=after,
533
+ limit=limit,
534
+ role=role,
535
+ year=year,
536
+ sort=sort,
537
+ desc=desc,
538
+ mode=mode,
539
+ )
540
+ if noun == "tracklist":
541
+ return _get_tracklist(client, entity_id, mode)
542
+ return _get_versions(
543
+ client,
544
+ entity_type,
545
+ entity_id,
546
+ ref,
547
+ page=page,
548
+ limit=limit,
549
+ country=country,
550
+ format=format,
551
+ label=label,
552
+ year=year,
553
+ sort=sort,
554
+ desc=desc,
555
+ mode=mode,
556
+ )
557
+
558
+
559
+ def _dispatch(
560
+ noun: str,
561
+ refs: tuple[str, ...],
562
+ *,
563
+ page: int | None,
564
+ after: str | None,
565
+ limit: int,
566
+ country: str | None,
567
+ format: str | None, # noqa: A002 # click option name for --format
568
+ label: str | None,
569
+ year: str | None,
570
+ sort: str | None,
571
+ desc: bool,
572
+ role: str | None,
573
+ verbose: bool,
574
+ compact: bool,
575
+ mode: Mode,
576
+ ) -> None:
577
+ """Shared dispatch for get, tracks, and price: one or many refs.
578
+
579
+ Several refs run in sequence, one API round trip each, so "compare these
580
+ pressings" is one command. Output is one block per ref in ref order,
581
+ blank-line separated, on stdout: a failing ref contributes its `✗` error
582
+ block and the rest still run; JSON becomes a list with `{"ref", "error"}`
583
+ items for failures. Exit 1 if any ref failed. With one ref the shape is
584
+ unchanged, including the single-error path (`fail()`: stderr text or a
585
+ JSON envelope).
586
+ """
587
+ flag_error = _flag_error(
588
+ noun,
589
+ refs,
590
+ page=page,
591
+ after=after,
592
+ role=role,
593
+ year=year,
594
+ sort=sort,
595
+ desc=desc,
596
+ )
597
+ if flag_error:
598
+ fail(ValueError(flag_error), json_output=mode.json)
599
+ if len(refs) > MAX_REFS:
600
+ fail(
601
+ ValueError(f"At most {MAX_REFS} refs per command ({len(refs)} given)."),
602
+ json_output=mode.json,
603
+ )
604
+
605
+ try:
606
+ client = get_client()
607
+ # classify() maps every exception to a coded, recovery-oriented error, so
608
+ # catching broadly is the point.
609
+ except Exception as e: # noqa: BLE001
610
+ fail(e, json_output=mode.json)
611
+
612
+ single = len(refs) == 1
613
+ blocks: list[Any] = [] # one per ref, in order: text/document or error
614
+ failed = False
615
+ for ref in refs:
616
+ context = f"{noun.title()} {ref}"
617
+ try:
618
+ blocks.append(
619
+ _run_one(
620
+ client,
621
+ noun,
622
+ ref,
623
+ page=page,
624
+ after=after,
625
+ limit=limit,
626
+ country=country,
627
+ format=format,
628
+ label=label,
629
+ year=year,
630
+ sort=sort,
631
+ desc=desc,
632
+ role=role,
633
+ verbose=verbose,
634
+ compact=compact,
635
+ mode=mode,
636
+ )
637
+ )
638
+ except Exception as e: # noqa: BLE001
639
+ if single:
640
+ fail(e, context, json_output=mode.json)
641
+ failed = True
642
+ if mode.json:
643
+ blocks.append({"ref": ref, "error": error_document(e, context)})
644
+ else:
645
+ blocks.append(format_error(e, context))
646
+
647
+ if mode.json:
648
+ dump(blocks[0] if single else blocks)
649
+ else:
650
+ print("\n\n".join(blocks))
651
+ if failed:
652
+ sys.exit(1)
653
+
654
+
655
+ _JSON_OPTIONS = [
656
+ click.option(
657
+ "--json",
658
+ "json_output",
659
+ is_flag=True,
660
+ default=False,
661
+ help="Output JSON (a compact projection of what the text view shows)",
662
+ ),
663
+ click.option(
664
+ "--full",
665
+ is_flag=True,
666
+ default=False,
667
+ help="With --json: the raw SDK model instead of the projection",
668
+ ),
669
+ ]
670
+
671
+
672
+ def _json_options(command: Any) -> Any:
673
+ for option in reversed(_JSON_OPTIONS):
674
+ command = option(command)
675
+ return command
676
+
677
+
678
+ @click.command()
679
+ @click.argument("noun", type=click.Choice(GET_NOUNS))
680
+ @click.argument("refs", nargs=-1, required=True, metavar="REF...")
681
+ @click.option(
682
+ "--after",
683
+ help="Continuation cursor copied from a previous Next page / Continue scan line",
684
+ )
685
+ @click.option(
686
+ "-c",
687
+ "--compact",
688
+ is_flag=True,
689
+ default=False,
690
+ help="release: replace the tracklist with a one-line Tracks: summary",
691
+ )
692
+ @click.option("--country", help="Filter versions by country")
693
+ @click.option(
694
+ "--desc", is_flag=True, default=False, help="With --sort: descending order"
695
+ )
696
+ @click.option("--format", "format_", help="Filter versions by format")
697
+ @_json_options
698
+ @click.option("--label", help="Filter versions by label")
699
+ @click.option("--limit", type=int, default=DEFAULT_LIMIT, help="Results per page")
700
+ @click.option("--page", type=int, help="Page number (server-side pages only)")
701
+ @click.option("--role", help="Filter releases by credit role (e.g., Main, Remix)")
702
+ @click.option(
703
+ "--sort",
704
+ help=(
705
+ "Server-side order. releases: year|title|format; "
706
+ "versions: released|title|format|label|catno|country"
707
+ ),
708
+ )
709
+ @click.option(
710
+ "-v",
711
+ "--verbose",
712
+ is_flag=True,
713
+ default=False,
714
+ help="release: also print notes, credits, and identifiers",
715
+ )
716
+ @click.option("--year", help="Filter versions by release year")
717
+ def get(
718
+ noun: str,
719
+ refs: tuple[str, ...],
720
+ json_output: bool,
721
+ full: bool,
722
+ after: str | None,
723
+ compact: bool,
724
+ country: str | None,
725
+ desc: bool,
726
+ format_: str | None,
727
+ label: str | None,
728
+ limit: int,
729
+ page: int | None,
730
+ role: str | None,
731
+ sort: str | None,
732
+ verbose: bool,
733
+ year: str | None,
734
+ ) -> None:
735
+ """Get entity details for one or more refs.
736
+
737
+ NOUN is the entity type: artist, credits, identifiers (alias: ids), label,
738
+ master, price, release, releases, tracklist, versions.
739
+ REF is a typed ref (@r123, @a456) or raw Discogs ID. Several refs run in
740
+ sequence (at most 10).
741
+ """
742
+ _dispatch(
743
+ NOUN_ALIASES.get(noun, noun),
744
+ refs,
745
+ page=page,
746
+ after=after,
747
+ limit=limit,
748
+ country=country,
749
+ format=format_,
750
+ label=label,
751
+ year=year,
752
+ sort=sort,
753
+ desc=desc,
754
+ role=role,
755
+ verbose=verbose,
756
+ compact=compact,
757
+ mode=_mode(json_output, full),
758
+ )
759
+
760
+
761
+ def _shortcut(noun: str, refs: tuple[str, ...], json_output: bool, full: bool) -> None:
762
+ _dispatch(
763
+ noun,
764
+ refs,
765
+ page=None,
766
+ after=None,
767
+ limit=DEFAULT_LIMIT,
768
+ country=None,
769
+ format=None,
770
+ label=None,
771
+ year=None,
772
+ sort=None,
773
+ desc=False,
774
+ role=None,
775
+ verbose=False,
776
+ compact=False,
777
+ mode=_mode(json_output, full),
778
+ )
779
+
780
+
781
+ @click.command()
782
+ @click.argument("refs", nargs=-1, required=True, metavar="REF...")
783
+ @_json_options
784
+ def tracks(refs: tuple[str, ...], json_output: bool, full: bool) -> None:
785
+ """Shortcut for: get tracklist REF..."""
786
+ _shortcut("tracklist", refs, json_output, full)
787
+
788
+
789
+ @click.command()
790
+ @click.argument("refs", nargs=-1, required=True, metavar="REF...")
791
+ @_json_options
792
+ def price(refs: tuple[str, ...], json_output: bool, full: bool) -> None:
793
+ """Shortcut for: get price REF..."""
794
+ _shortcut("price", refs, json_output, full)