metadatarr 0.1.0__tar.gz

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.
Files changed (119) hide show
  1. metadatarr-0.1.0/LICENSE +21 -0
  2. metadatarr-0.1.0/PKG-INFO +430 -0
  3. metadatarr-0.1.0/README.md +370 -0
  4. metadatarr-0.1.0/examples/alien_trilogy_physical_and_fanedits.py +353 -0
  5. metadatarr-0.1.0/examples/arr_search.py +22 -0
  6. metadatarr-0.1.0/examples/books.py +29 -0
  7. metadatarr-0.1.0/examples/channel_scanner.py +167 -0
  8. metadatarr-0.1.0/examples/channel_to_metadata.py +301 -0
  9. metadatarr-0.1.0/examples/cross_provider_search.py +72 -0
  10. metadatarr-0.1.0/examples/discogs_music_video.py +308 -0
  11. metadatarr-0.1.0/examples/fanedit_discovery.py +108 -0
  12. metadatarr-0.1.0/examples/fanedit_picker.py +146 -0
  13. metadatarr-0.1.0/examples/learn/01_first_lookup.py +24 -0
  14. metadatarr-0.1.0/examples/learn/02_three_axis_routing.py +42 -0
  15. metadatarr-0.1.0/examples/learn/03_search_vs_resolve.py +38 -0
  16. metadatarr-0.1.0/examples/learn/04_consolidator_anatomy.py +53 -0
  17. metadatarr-0.1.0/examples/learn/05_variants_fanout.py +34 -0
  18. metadatarr-0.1.0/examples/learn/06_direct_provider.py +40 -0
  19. metadatarr-0.1.0/examples/learn/07_voice_agent_routing.py +58 -0
  20. metadatarr-0.1.0/examples/learn/08_writing_a_provider.py +77 -0
  21. metadatarr-0.1.0/examples/learn/09_caching_and_performance.py +41 -0
  22. metadatarr-0.1.0/examples/library_cut_dedup.py +200 -0
  23. metadatarr-0.1.0/examples/live/_common.py +27 -0
  24. metadatarr-0.1.0/examples/live/check_anilist.py +27 -0
  25. metadatarr-0.1.0/examples/live/check_apple_podcasts.py +20 -0
  26. metadatarr-0.1.0/examples/live/check_helpers.py +33 -0
  27. metadatarr-0.1.0/examples/live/check_jikan.py +26 -0
  28. metadatarr-0.1.0/examples/live/check_librivox.py +21 -0
  29. metadatarr-0.1.0/examples/live/check_license.py +41 -0
  30. metadatarr-0.1.0/examples/live/check_openlibrary.py +23 -0
  31. metadatarr-0.1.0/examples/live/check_programme_schedule.py +31 -0
  32. metadatarr-0.1.0/examples/live/check_release_relation.py +28 -0
  33. metadatarr-0.1.0/examples/live/check_tvmaze.py +22 -0
  34. metadatarr-0.1.0/examples/music.py +23 -0
  35. metadatarr-0.1.0/examples/physical_disc_verify.py +203 -0
  36. metadatarr-0.1.0/examples/resolve_artist_merge.py +187 -0
  37. metadatarr-0.1.0/examples/resolve_fanedit_by_imdb.py +83 -0
  38. metadatarr-0.1.0/examples/resolve_mapping_demo.py +126 -0
  39. metadatarr-0.1.0/examples/resolve_movie.py +46 -0
  40. metadatarr-0.1.0/examples/resolve_variants_album.py +48 -0
  41. metadatarr-0.1.0/examples/resolve_variants_movie.py +46 -0
  42. metadatarr-0.1.0/examples/streams_music.py +95 -0
  43. metadatarr-0.1.0/examples/streams_radio.py +127 -0
  44. metadatarr-0.1.0/examples/streams_video.py +86 -0
  45. metadatarr-0.1.0/examples/title_parser_demo.py +44 -0
  46. metadatarr-0.1.0/examples/variant_album_editions.py +86 -0
  47. metadatarr-0.1.0/examples/variant_custom_provider.py +136 -0
  48. metadatarr-0.1.0/examples/variant_disambiguation_cuts.py +59 -0
  49. metadatarr-0.1.0/examples/variant_fanedit_detail.py +66 -0
  50. metadatarr-0.1.0/examples/variant_provider_registry.py +69 -0
  51. metadatarr-0.1.0/examples/variant_regional_film.py +78 -0
  52. metadatarr-0.1.0/examples/variant_resolve_with_without.py +67 -0
  53. metadatarr-0.1.0/examples/variant_signals.py +113 -0
  54. metadatarr-0.1.0/examples/variant_source_format.py +68 -0
  55. metadatarr-0.1.0/examples/vhs_legacy_live_test.py +103 -0
  56. metadatarr-0.1.0/examples/vhs_legacy_to_physical.py +139 -0
  57. metadatarr-0.1.0/examples/video.py +29 -0
  58. metadatarr-0.1.0/metadatarr/__init__.py +56 -0
  59. metadatarr-0.1.0/metadatarr/client.py +1362 -0
  60. metadatarr-0.1.0/metadatarr/data/__init__.py +0 -0
  61. metadatarr-0.1.0/metadatarr/data/mappings.toml +26 -0
  62. metadatarr-0.1.0/metadatarr/models.py +824 -0
  63. metadatarr-0.1.0/metadatarr/resolve/__init__.py +93 -0
  64. metadatarr-0.1.0/metadatarr/resolve/_cache.py +126 -0
  65. metadatarr-0.1.0/metadatarr/resolve/_http_cache.py +227 -0
  66. metadatarr-0.1.0/metadatarr/resolve/base.py +403 -0
  67. metadatarr-0.1.0/metadatarr/resolve/entities.py +318 -0
  68. metadatarr-0.1.0/metadatarr/resolve/mappings.py +358 -0
  69. metadatarr-0.1.0/metadatarr/resolve/providers/__init__.py +38 -0
  70. metadatarr-0.1.0/metadatarr/resolve/providers/anilist.py +172 -0
  71. metadatarr-0.1.0/metadatarr/resolve/providers/annas_archive.py +82 -0
  72. metadatarr-0.1.0/metadatarr/resolve/providers/audiodb.py +173 -0
  73. metadatarr-0.1.0/metadatarr/resolve/providers/bandcamp.py +297 -0
  74. metadatarr-0.1.0/metadatarr/resolve/providers/bluray_com.py +81 -0
  75. metadatarr-0.1.0/metadatarr/resolve/providers/discogs.py +139 -0
  76. metadatarr-0.1.0/metadatarr/resolve/providers/dvdcompare.py +177 -0
  77. metadatarr-0.1.0/metadatarr/resolve/providers/jikan.py +191 -0
  78. metadatarr-0.1.0/metadatarr/resolve/providers/librivox.py +113 -0
  79. metadatarr-0.1.0/metadatarr/resolve/providers/metal_archives.py +196 -0
  80. metadatarr-0.1.0/metadatarr/resolve/providers/musicbrainz.py +242 -0
  81. metadatarr-0.1.0/metadatarr/resolve/providers/openlibrary.py +122 -0
  82. metadatarr-0.1.0/metadatarr/resolve/providers/podcast_index.py +93 -0
  83. metadatarr-0.1.0/metadatarr/resolve/providers/pyfanedit.py +100 -0
  84. metadatarr-0.1.0/metadatarr/resolve/providers/servarr_proxy.py +216 -0
  85. metadatarr-0.1.0/metadatarr/resolve/providers/soundcloud.py +217 -0
  86. metadatarr-0.1.0/metadatarr/resolve/providers/tvmaze.py +139 -0
  87. metadatarr-0.1.0/metadatarr/resolve/providers/wikidata.py +194 -0
  88. metadatarr-0.1.0/metadatarr/resolve/providers/youtube.py +127 -0
  89. metadatarr-0.1.0/metadatarr/resolve/providers/youtube_music.py +158 -0
  90. metadatarr-0.1.0/metadatarr/resolve/sidecar.py +130 -0
  91. metadatarr-0.1.0/metadatarr/resolve/title_parser.py +41 -0
  92. metadatarr-0.1.0/metadatarr/version.py +8 -0
  93. metadatarr-0.1.0/metadatarr.egg-info/PKG-INFO +430 -0
  94. metadatarr-0.1.0/metadatarr.egg-info/SOURCES.txt +117 -0
  95. metadatarr-0.1.0/metadatarr.egg-info/dependency_links.txt +1 -0
  96. metadatarr-0.1.0/metadatarr.egg-info/requires.txt +15 -0
  97. metadatarr-0.1.0/metadatarr.egg-info/top_level.txt +5 -0
  98. metadatarr-0.1.0/pyproject.toml +81 -0
  99. metadatarr-0.1.0/setup.cfg +4 -0
  100. metadatarr-0.1.0/tests/__init__.py +0 -0
  101. metadatarr-0.1.0/tests/test_bandcamp_slug.py +40 -0
  102. metadatarr-0.1.0/tests/test_cache_and_resolve.py +163 -0
  103. metadatarr-0.1.0/tests/test_clients.py +130 -0
  104. metadatarr-0.1.0/tests/test_clients_deep.py +408 -0
  105. metadatarr-0.1.0/tests/test_enrich.py +436 -0
  106. metadatarr-0.1.0/tests/test_entities.py +278 -0
  107. metadatarr-0.1.0/tests/test_isbn.py +48 -0
  108. metadatarr-0.1.0/tests/test_mappings_toml.py +119 -0
  109. metadatarr-0.1.0/tests/test_models.py +79 -0
  110. metadatarr-0.1.0/tests/test_package.py +24 -0
  111. metadatarr-0.1.0/tests/test_physical_clients.py +828 -0
  112. metadatarr-0.1.0/tests/test_provider_candidates.py +100 -0
  113. metadatarr-0.1.0/tests/test_providers_registry.py +59 -0
  114. metadatarr-0.1.0/tests/test_pyfanedit.py +159 -0
  115. metadatarr-0.1.0/tests/test_resolve_base.py +136 -0
  116. metadatarr-0.1.0/tests/test_search.py +122 -0
  117. metadatarr-0.1.0/tests/test_sidecar.py +88 -0
  118. metadatarr-0.1.0/tests/test_signals.py +107 -0
  119. metadatarr-0.1.0/tests/test_streams.py +200 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JarbasAi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,430 @@
1
+ Metadata-Version: 2.4
2
+ Name: metadatarr
3
+ Version: 0.1.0
4
+ Summary: Pydantic-powered Python clients and cross-source resolver for media metadata (Servarr proxies, OpenLibrary, MusicBrainz, TMDB, TVmaze, AudioDB, Anna's Archive, Wikidata, Bandcamp, SoundCloud, YouTube, Metal Archives)
5
+ Author-email: JarbasAi <jarbasai@mailfence.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 JarbasAi
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/TigreGotico/metadatarr
29
+ Project-URL: Repository, https://github.com/TigreGotico/metadatarr
30
+ Project-URL: Issues, https://github.com/TigreGotico/metadatarr/issues
31
+ Keywords: metadata,servarr,sonarr,radarr,lidarr,musicbrainz,openlibrary,tmdb,tvmaze,pydantic
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.9
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: License :: OSI Approved :: MIT License
40
+ Classifier: Operating System :: OS Independent
41
+ Classifier: Topic :: Internet :: WWW/HTTP
42
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
43
+ Classifier: Topic :: Multimedia
44
+ Requires-Python: >=3.9
45
+ Description-Content-Type: text/markdown
46
+ License-File: LICENSE
47
+ Requires-Dist: requests>=2.25.0
48
+ Requires-Dist: pydantic>=2.0.0
49
+ Requires-Dist: beautifulsoup4>=4.10.0
50
+ Requires-Dist: tomli>=1.1.0; python_version < "3.11"
51
+ Requires-Dist: mediavocab>=0.1.0
52
+ Requires-Dist: pyfanedit
53
+ Requires-Dist: pymetal>=1.0.0a1
54
+ Requires-Dist: tutubo>=0.1.0
55
+ Requires-Dist: py_bandcamp
56
+ Requires-Dist: nuvem_de_som
57
+ Provides-Extra: test
58
+ Requires-Dist: pytest>=7; extra == "test"
59
+ Dynamic: license-file
60
+
61
+ # metadatarr
62
+
63
+ > **One library. Every catalogue. Zero API keys.**
64
+
65
+ [![PyPI](https://img.shields.io/pypi/v/metadatarr)](https://pypi.org/project/metadatarr/)
66
+ [![Python](https://img.shields.io/pypi/pyversions/metadatarr)](https://pypi.org/project/metadatarr/)
67
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
68
+ [![Build](https://github.com/TigreGotico/metadatarr/actions/workflows/build-tests.yml/badge.svg)](https://github.com/TigreGotico/metadatarr/actions/workflows/build-tests.yml)
69
+
70
+ Pydantic-powered Python clients and a cross-source **entity resolver** for media metadata.
71
+ Talk to the public catalogues that the *arr ecosystem, media managers, and libraries rely on —
72
+ then fuse the answers into a single, de-duplicated record with a canonical set of external IDs.
73
+
74
+ ```python
75
+ from metadatarr.resolve import resolve
76
+ from mediavocab import Signals, MediaType
77
+
78
+ result = resolve(Signals(title="Inception", year=2010, medium=MediaType.MOVIE))
79
+
80
+ print(result.external_ids.tmdb_movie) # 27205
81
+ print(result.external_ids.imdb) # tt1375666
82
+ print(result.external_ids.wikidata) # Q25188
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Why metadatarr?
88
+
89
+ Most media tools need to cross-reference the same work across Sonarr, MusicBrainz, Discogs,
90
+ and Wikidata — but every API has a different shape, auth model, and concept of "the same thing."
91
+ `metadatarr` handles all of that:
92
+
93
+ - **Typed clients** — every response parsed into Pydantic V2 models; no dict spelunking.
94
+ - **Keyless by default** — every built-in provider works without registration or tokens.
95
+ - **Cross-source resolver** — fans out to every relevant provider in parallel, conflict-checks
96
+ the results, and merges winners into one `ResolveResult` with `ExternalIds`.
97
+ - **Variant fan-out** — one flag (`include_variants=True`) and the resolver collects every
98
+ known cut, edition, or fanedit of a work.
99
+ - **Batteries-included** — pyfanedit, pymetal, tutubo, py_bandcamp, and nuvem_de_som are all
100
+ core dependencies; no optional-extra juggling required.
101
+
102
+ ---
103
+
104
+ ## Installation
105
+
106
+ ```bash
107
+ pip install metadatarr
108
+ ```
109
+
110
+ All first-party scrapers (pyfanedit, pymetal, tutubo, py_bandcamp, nuvem_de_som) are core
111
+ dependencies — no extras required. The only optional extra is `[test]` for running the test suite.
112
+
113
+ ---
114
+
115
+ ## Direct clients
116
+
117
+ Each client is a thin, typed wrapper around one data source.
118
+
119
+ | Client | Source | What you get |
120
+ |---|---|---|
121
+ | `ArrMetadataClient` | Servarr proxies (Skyhook / Radarr / Lidarr) | TV shows, movies, artists — same data that powers Sonarr/Radarr/Lidarr |
122
+ | `OpenLibraryClient` | openlibrary.org | Works, editions, authors, ISBN lookup, covers |
123
+ | `BookInfoClient` | rreading-glasses (Goodreads / Hardcover) | Book metadata via Goodreads / Hardcover |
124
+ | `AnnasArchiveClient` | Anna's Archive mirrors | Book search (HTML scrape) |
125
+ | `AudioDBClient` | theaudiodb.com | Artists, albums, tracks |
126
+ | `TVmazeClient` | tvmaze.com | Shows, seasons, episodes, cast, people |
127
+ | `BlurayComClient` | blu-ray.com | Physical Blu-ray specs — audio tracks, region codes, extras |
128
+ | `DVDCompareClient` | dvdcompare.net | Regional release comparison, cut runtimes, version notes |
129
+ | `DiscogsClient` | discogs.com | Vinyl, CD, cassette releases; `search_video()` for LaserDiscs / concert VHS / music DVDs |
130
+
131
+ ```python
132
+ from metadatarr import ArrMetadataClient, OpenLibraryClient, AudioDBClient, TVmazeClient
133
+
134
+ # Movies & TV via Servarr proxies
135
+ arr = ArrMetadataClient()
136
+ movie = arr.search_movie("Alien")[0]
137
+ series = arr.search_series("The Boys")[0]
138
+ artist = arr.search_artist("Moonsorrow")[0]
139
+ print(movie.tmdb_id, series.tvdb_id, artist.mb_id)
140
+
141
+ # Books
142
+ ol = OpenLibraryClient()
143
+ hit = ol.search("The Hobbit", limit=1)[0]
144
+ print(hit.key, hit.first_publish_year)
145
+
146
+ # Music
147
+ db = AudioDBClient()
148
+ alb = db.search_album("Voimasta ja Kunniasta")[0]
149
+ print(alb.id_album, alb.str_genre)
150
+
151
+ # TV
152
+ tv = TVmazeClient()
153
+ show = tv.singlesearch("Severance")
154
+ print(show.id, show.network.name)
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Cross-source resolver
160
+
161
+ When you have a title, a year, or a noisy filename and need a canonical identity across every
162
+ platform, the resolver fans out, conflict-checks, and merges:
163
+
164
+ ```python
165
+ from metadatarr.resolve import resolve
166
+ from mediavocab import Signals, MediaType
167
+
168
+ # A basic lookup — metadatarr queries all active providers concurrently
169
+ result = resolve(Signals(title="OK Computer", artist="Radiohead", medium=MediaType.MUSIC))
170
+
171
+ print(result.external_ids.musicbrainz_release_group) # MusicBrainz MBID
172
+ print(result.external_ids.wikidata) # Wikidata Q-id
173
+ print(result.external_ids.extra.get("bandcamp_album_id"))
174
+
175
+ # Inspect what was accepted and what was rejected
176
+ for m in result.accepted:
177
+ print(f" ✓ {m.provider:<20} confidence={m.confidence:.2f}")
178
+ for d in result.conflicts:
179
+ fields = ", ".join(f"{c.signal}({c.ours}≠{c.theirs})" for c in d.fields)
180
+ print(f" ✗ {d.provider:<20} clashed on {fields}")
181
+ ```
182
+
183
+ ### Signals — tell the resolver what you know
184
+
185
+ ```python
186
+ from mediavocab import Signals, MediaType
187
+
188
+ signals = Signals(
189
+ title = "Alien",
190
+ year = 1979,
191
+ medium = MediaType.MOVIE,
192
+ runtime = 6900, # seconds — used for cut-disambiguation
193
+ language = "en",
194
+ country = "US",
195
+ )
196
+ ```
197
+
198
+ Pass as much or as little as you have. Every field is optional. The more context you
199
+ provide, the better providers can filter and the more aggressively conflicts are detected.
200
+
201
+ **MediaType values:** Comes from mediavocab — 18 canonical values (`MOVIE`, `EPISODIC_SERIES`, `TV`, `MUSIC`, `MUSIC_VIDEO`, `PODCAST`, `BOOK`, `COMIC`, `GAME`, `AUDIOBOOK`, `AUDIO_DRAMA`, `RADIO`, `INTERACTIVE_FICTION`, `SOUND_EFFECT`, `AMBIENT_SOUNDS`, `PLAYLIST`, `GENERIC`, `NOT_MEDIA`). See the [mediavocab spec §4.1](https://github.com/TigreGotico/mediavocab/blob/dev/docs/mediavocab_spec.md).
202
+
203
+ ### Variant fan-out — editions, cuts, fanedits
204
+
205
+ ```python
206
+ from metadatarr.resolve import resolve
207
+ from mediavocab import Signals, MediaType
208
+ from metadatarr.resolve.entities import EntityRole
209
+
210
+ result = resolve(Signals(
211
+ title = "Alien",
212
+ year = 1979,
213
+ medium = MediaType.MOVIE,
214
+ include_variants= True, # ← triggers second pass
215
+ ))
216
+
217
+ for entity in result.variants:
218
+ print(entity.name, entity.external_ids.fanedit_id)
219
+ # Alien: Covenant Cut, Alien: The Director's Cut, ...
220
+ ```
221
+
222
+ With `include_variants=True` the resolver runs a second pass calling `list_variants()` on
223
+ every active provider:
224
+ - **pyfanedit** — queries fanedit.org (IFDB) for fan-edited cuts of the movie
225
+ - **musicbrainz** — expands a release-group MBID to its individual releases (editions, remasters, regional pressings)
226
+
227
+ ### ExternalIds — every platform in one object
228
+
229
+ ```python
230
+ from mediavocab import ExternalIds
231
+
232
+ ids = result.external_ids
233
+ print(ids.tmdb_movie) # int
234
+ print(ids.imdb) # "tt0078748"
235
+ print(ids.musicbrainz_release_group) # UUID str
236
+ print(ids.wikidata) # "Q103569"
237
+ print(ids.extra.get("bandcamp_album_id")) # platform extras
238
+ ```
239
+
240
+ First-class typed fields: `musicbrainz_*`, `imdb`, `tmdb_movie`, `tmdb_tv`, `tvdb`,
241
+ `isbn_10`, `isbn_13`, `olid`, `goodreads`, `wikidata`, `metal_archives_*`,
242
+ `fanedit_id`, `derived_from_imdb`, `discogs_release`, `bluray_com_id`, `dvdcompare_id`, …
243
+ plus an `extra` dict for platform-specific IDs (Bandcamp, SoundCloud, YouTube Music, …).
244
+
245
+ ---
246
+
247
+ ## Built-in providers
248
+
249
+ All providers are keyless. All dependencies are bundled in the core install.
250
+
251
+ Routing is **three-axis** — `media`, `modality`, and `genre_filter`. Pass `modality` on
252
+ `Signals` to route a `MediaType.GENERIC` query to audio-only or video-only providers.
253
+ See [`docs/resolve.md`](docs/resolve.md#three-axis-routing-gate) for details.
254
+
255
+ | Provider | Source | MediaType | Modality |
256
+ |---|---|---|---|
257
+ | `skyhook` | Servarr proxies | Movie, EpisodicSeries, Music, Book | universal |
258
+ | `musicbrainz` | MusicBrainz API | Music | AUDIO |
259
+ | `audiodb` | TheAudioDB | Music | AUDIO |
260
+ | `tvmaze` | TVmaze public API | EpisodicSeries | VIDEO |
261
+ | `anilist` | AniList GraphQL API | Movie, EpisodicSeries, Comic | VIDEO + TEXT |
262
+ | `jikan_anime` | Jikan (MyAnimeList) | Movie, EpisodicSeries | VIDEO |
263
+ | `jikan_manga` | Jikan (MyAnimeList) | Comic | TEXT |
264
+ | `librivox` | LibriVox API | Audiobook | AUDIO |
265
+ | `apple_podcasts` | Apple Podcasts search | Podcast, AudioDrama | AUDIO |
266
+ | `wikidata` | Wikidata API | All | universal |
267
+ | `discogs` | Discogs API | Music, MusicVideo, Generic | AUDIO + VIDEO |
268
+ | `bluray_com` | blu-ray.com scraper | Movie | VIDEO |
269
+ | `dvdcompare` | dvdcompare.net scraper | Movie | VIDEO |
270
+ | `pyfanedit` | fanedit.org / IFDB | Movie (variants) | VIDEO |
271
+ | `bandcamp` | Bandcamp | Music | AUDIO |
272
+ | `soundcloud` | SoundCloud | Music | AUDIO |
273
+ | `youtube_music` | YouTube Music | Music | AUDIO |
274
+ | `youtube` | YouTube | Video, Podcast, Generic | universal |
275
+ | `metal_archives` | Encyclopaedia Metallum | Music | AUDIO |
276
+ | `openlibrary` | OpenLibrary | Book | TEXT |
277
+ | `annas_archive` | Anna's Archive | Book | TEXT |
278
+
279
+ **YouTube vs YouTube Music** — these are intentionally separate providers.
280
+ `youtube` only emits channel IDs and refuses `MediaType.MUSIC` lookups (video IDs aren't
281
+ canonical music identities). `youtube_music` has proper entity records — stable `browseId`
282
+ values for artists and albums that are safe to treat as cross-references.
283
+
284
+ ---
285
+
286
+ ## Identity mappings
287
+
288
+ Some artists and labels are the same entity across platforms but no database records the link.
289
+ Declare it once in a TOML file and every resolver run picks it up automatically:
290
+
291
+ ```toml
292
+ # ~/.config/metadatarr/mappings.toml
293
+
294
+ [[artist]]
295
+ name = "Acidkid / Piratech"
296
+ soundcloud_artist_url = "https://soundcloud.com/acidkid"
297
+ bandcamp_artist_url = "https://piratech.bandcamp.com/"
298
+
299
+ [[artist]]
300
+ name = "Moonsorrow"
301
+ musicbrainz_artist = "6a0a7b9b-9e12-4e1c-b91d-67cedf98a6c3"
302
+ bandcamp_band_id = "3498887240"
303
+ metal_archives_band= 27
304
+ ```
305
+
306
+ The package ships a curated `metadatarr/data/mappings.toml`. Your user file at
307
+ `~/.config/metadatarr/mappings.toml` extends it — entries that share any identifier are merged,
308
+ new entries are appended. Send a PR to add publicly-verifiable cross-platform links to the
309
+ package file.
310
+
311
+ ---
312
+
313
+ ## Writing a custom provider
314
+
315
+ ```python
316
+ from typing import Optional
317
+ from metadatarr.resolve.base import MetadataProvider, ProviderMatch, register
318
+ from mediavocab import ExternalIds
319
+ from mediavocab import Signals, MediaType
320
+
321
+
322
+ class MyProvider(MetadataProvider):
323
+ name = "my_provider"
324
+ media = {MediaType.MUSIC}
325
+
326
+ def is_available(self) -> bool:
327
+ return True
328
+
329
+ def lookup(self, signals: Signals) -> Optional[ProviderMatch]:
330
+ if not signals.title:
331
+ return None
332
+ result = my_api.search(signals.title)
333
+ if not result:
334
+ return None
335
+ return ProviderMatch(
336
+ provider = self.name,
337
+ confidence = 0.7,
338
+ signals = Signals(title=result["title"], medium=MediaType.MUSIC),
339
+ external_ids = ExternalIds(
340
+ musicbrainz_artist = result.get("mbid"),
341
+ extra = {"my_platform_id": str(result["id"])},
342
+ ),
343
+ )
344
+
345
+
346
+ register(MyProvider())
347
+ ```
348
+
349
+ Provider guidelines:
350
+ - **Guard optional imports** — wrap `import my_lib` in `try/except ImportError`, set `self._available = False` on failure.
351
+ - **Canonical IDs only** — numeric platform IDs are stable; URL slugs are not. Store URLs as `*_url` extra keys.
352
+ - **Refuse wrong mediums** — return `None` if `signals.medium` isn't in your `media` set.
353
+ - **Confidence guide** — 0.9 for exact-ID lookups, 0.7 for strong-signal search, 0.5–0.6 for fuzzy/unreliable sources.
354
+
355
+ ---
356
+
357
+ ## Physical media
358
+
359
+ `BlurayComClient` and `DVDCompareClient` expose Blu-ray and DVD edition data that no
360
+ structured API covers — region codes, audio track specs, cut runtimes, regional extras:
361
+
362
+ ```python
363
+ from metadatarr.resolve.providers.bluray_com import BlurayComProvider
364
+ from metadatarr.resolve.providers.dvdcompare import DVDCompareProvider
365
+ from mediavocab import Signals, MediaType
366
+
367
+ signals = Signals(title="Moon", year=2009, medium=MediaType.MOVIE)
368
+
369
+ bluray = BlurayComProvider()
370
+ match = bluray.lookup(signals)
371
+ if match:
372
+ print(match.external_ids.bluray_com_id)
373
+
374
+ dvd = DVDCompareProvider()
375
+ match = dvd.lookup(signals)
376
+ if match:
377
+ print(match.external_ids.dvdcompare_id)
378
+ ```
379
+
380
+ See [`docs/physical-disc.md`](docs/physical-disc.md) for a full walkthrough.
381
+
382
+ ---
383
+
384
+ ## Caching and concurrency
385
+
386
+ `resolve()` is concurrent (default 8 workers via `ThreadPoolExecutor`) and process-level cached:
387
+
388
+ ```python
389
+ from metadatarr.resolve._cache import cache
390
+
391
+ cache().hits # int — cached lookups served
392
+ cache().misses # int — network hits
393
+ cache().clear() # force re-fetch (e.g. after adding a new provider)
394
+ ```
395
+
396
+ Both hits and misses are cached, so failed lookups don't re-hit the network on retry.
397
+ Pass `resolve(signals, max_workers=N)` to tune parallelism.
398
+
399
+ ---
400
+
401
+ ## Documentation
402
+
403
+ | Doc | Contents |
404
+ |---|---|
405
+ | [`docs/getting-started.md`](docs/getting-started.md) | Install, first calls, common patterns |
406
+ | [`docs/models.md`](docs/models.md) | Full Pydantic model reference |
407
+ | [`docs/resolve.md`](docs/resolve.md) | Signals, providers, ResolveResult, conflict detection |
408
+ | [`docs/providers.md`](docs/providers.md) | Provider catalogue — config, optional deps, caveats |
409
+ | [`docs/recipes.md`](docs/recipes.md) | End-to-end snippets for common tasks |
410
+ | [`docs/physical-disc.md`](docs/physical-disc.md) | Blu-ray / DVD edition data |
411
+ | [`docs/troubleshooting.md`](docs/troubleshooting.md) | Gotchas and FAQ |
412
+ | [`docs/clients/`](docs/clients/) | Per-client deep dives |
413
+ | [`examples/`](examples/) | One focused script per use case |
414
+
415
+ ---
416
+
417
+ ## Testing
418
+
419
+ ```bash
420
+ pip install -e ".[test]"
421
+ pytest
422
+ ```
423
+
424
+ Tests are fully offline — all HTTP calls are stubbed with fixture files.
425
+
426
+ ---
427
+
428
+ ## License
429
+
430
+ MIT — see [LICENSE](LICENSE).