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.
- metadatarr-0.1.0/LICENSE +21 -0
- metadatarr-0.1.0/PKG-INFO +430 -0
- metadatarr-0.1.0/README.md +370 -0
- metadatarr-0.1.0/examples/alien_trilogy_physical_and_fanedits.py +353 -0
- metadatarr-0.1.0/examples/arr_search.py +22 -0
- metadatarr-0.1.0/examples/books.py +29 -0
- metadatarr-0.1.0/examples/channel_scanner.py +167 -0
- metadatarr-0.1.0/examples/channel_to_metadata.py +301 -0
- metadatarr-0.1.0/examples/cross_provider_search.py +72 -0
- metadatarr-0.1.0/examples/discogs_music_video.py +308 -0
- metadatarr-0.1.0/examples/fanedit_discovery.py +108 -0
- metadatarr-0.1.0/examples/fanedit_picker.py +146 -0
- metadatarr-0.1.0/examples/learn/01_first_lookup.py +24 -0
- metadatarr-0.1.0/examples/learn/02_three_axis_routing.py +42 -0
- metadatarr-0.1.0/examples/learn/03_search_vs_resolve.py +38 -0
- metadatarr-0.1.0/examples/learn/04_consolidator_anatomy.py +53 -0
- metadatarr-0.1.0/examples/learn/05_variants_fanout.py +34 -0
- metadatarr-0.1.0/examples/learn/06_direct_provider.py +40 -0
- metadatarr-0.1.0/examples/learn/07_voice_agent_routing.py +58 -0
- metadatarr-0.1.0/examples/learn/08_writing_a_provider.py +77 -0
- metadatarr-0.1.0/examples/learn/09_caching_and_performance.py +41 -0
- metadatarr-0.1.0/examples/library_cut_dedup.py +200 -0
- metadatarr-0.1.0/examples/live/_common.py +27 -0
- metadatarr-0.1.0/examples/live/check_anilist.py +27 -0
- metadatarr-0.1.0/examples/live/check_apple_podcasts.py +20 -0
- metadatarr-0.1.0/examples/live/check_helpers.py +33 -0
- metadatarr-0.1.0/examples/live/check_jikan.py +26 -0
- metadatarr-0.1.0/examples/live/check_librivox.py +21 -0
- metadatarr-0.1.0/examples/live/check_license.py +41 -0
- metadatarr-0.1.0/examples/live/check_openlibrary.py +23 -0
- metadatarr-0.1.0/examples/live/check_programme_schedule.py +31 -0
- metadatarr-0.1.0/examples/live/check_release_relation.py +28 -0
- metadatarr-0.1.0/examples/live/check_tvmaze.py +22 -0
- metadatarr-0.1.0/examples/music.py +23 -0
- metadatarr-0.1.0/examples/physical_disc_verify.py +203 -0
- metadatarr-0.1.0/examples/resolve_artist_merge.py +187 -0
- metadatarr-0.1.0/examples/resolve_fanedit_by_imdb.py +83 -0
- metadatarr-0.1.0/examples/resolve_mapping_demo.py +126 -0
- metadatarr-0.1.0/examples/resolve_movie.py +46 -0
- metadatarr-0.1.0/examples/resolve_variants_album.py +48 -0
- metadatarr-0.1.0/examples/resolve_variants_movie.py +46 -0
- metadatarr-0.1.0/examples/streams_music.py +95 -0
- metadatarr-0.1.0/examples/streams_radio.py +127 -0
- metadatarr-0.1.0/examples/streams_video.py +86 -0
- metadatarr-0.1.0/examples/title_parser_demo.py +44 -0
- metadatarr-0.1.0/examples/variant_album_editions.py +86 -0
- metadatarr-0.1.0/examples/variant_custom_provider.py +136 -0
- metadatarr-0.1.0/examples/variant_disambiguation_cuts.py +59 -0
- metadatarr-0.1.0/examples/variant_fanedit_detail.py +66 -0
- metadatarr-0.1.0/examples/variant_provider_registry.py +69 -0
- metadatarr-0.1.0/examples/variant_regional_film.py +78 -0
- metadatarr-0.1.0/examples/variant_resolve_with_without.py +67 -0
- metadatarr-0.1.0/examples/variant_signals.py +113 -0
- metadatarr-0.1.0/examples/variant_source_format.py +68 -0
- metadatarr-0.1.0/examples/vhs_legacy_live_test.py +103 -0
- metadatarr-0.1.0/examples/vhs_legacy_to_physical.py +139 -0
- metadatarr-0.1.0/examples/video.py +29 -0
- metadatarr-0.1.0/metadatarr/__init__.py +56 -0
- metadatarr-0.1.0/metadatarr/client.py +1362 -0
- metadatarr-0.1.0/metadatarr/data/__init__.py +0 -0
- metadatarr-0.1.0/metadatarr/data/mappings.toml +26 -0
- metadatarr-0.1.0/metadatarr/models.py +824 -0
- metadatarr-0.1.0/metadatarr/resolve/__init__.py +93 -0
- metadatarr-0.1.0/metadatarr/resolve/_cache.py +126 -0
- metadatarr-0.1.0/metadatarr/resolve/_http_cache.py +227 -0
- metadatarr-0.1.0/metadatarr/resolve/base.py +403 -0
- metadatarr-0.1.0/metadatarr/resolve/entities.py +318 -0
- metadatarr-0.1.0/metadatarr/resolve/mappings.py +358 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/__init__.py +38 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/anilist.py +172 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/annas_archive.py +82 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/audiodb.py +173 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/bandcamp.py +297 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/bluray_com.py +81 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/discogs.py +139 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/dvdcompare.py +177 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/jikan.py +191 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/librivox.py +113 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/metal_archives.py +196 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/musicbrainz.py +242 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/openlibrary.py +122 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/podcast_index.py +93 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/pyfanedit.py +100 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/servarr_proxy.py +216 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/soundcloud.py +217 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/tvmaze.py +139 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/wikidata.py +194 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/youtube.py +127 -0
- metadatarr-0.1.0/metadatarr/resolve/providers/youtube_music.py +158 -0
- metadatarr-0.1.0/metadatarr/resolve/sidecar.py +130 -0
- metadatarr-0.1.0/metadatarr/resolve/title_parser.py +41 -0
- metadatarr-0.1.0/metadatarr/version.py +8 -0
- metadatarr-0.1.0/metadatarr.egg-info/PKG-INFO +430 -0
- metadatarr-0.1.0/metadatarr.egg-info/SOURCES.txt +117 -0
- metadatarr-0.1.0/metadatarr.egg-info/dependency_links.txt +1 -0
- metadatarr-0.1.0/metadatarr.egg-info/requires.txt +15 -0
- metadatarr-0.1.0/metadatarr.egg-info/top_level.txt +5 -0
- metadatarr-0.1.0/pyproject.toml +81 -0
- metadatarr-0.1.0/setup.cfg +4 -0
- metadatarr-0.1.0/tests/__init__.py +0 -0
- metadatarr-0.1.0/tests/test_bandcamp_slug.py +40 -0
- metadatarr-0.1.0/tests/test_cache_and_resolve.py +163 -0
- metadatarr-0.1.0/tests/test_clients.py +130 -0
- metadatarr-0.1.0/tests/test_clients_deep.py +408 -0
- metadatarr-0.1.0/tests/test_enrich.py +436 -0
- metadatarr-0.1.0/tests/test_entities.py +278 -0
- metadatarr-0.1.0/tests/test_isbn.py +48 -0
- metadatarr-0.1.0/tests/test_mappings_toml.py +119 -0
- metadatarr-0.1.0/tests/test_models.py +79 -0
- metadatarr-0.1.0/tests/test_package.py +24 -0
- metadatarr-0.1.0/tests/test_physical_clients.py +828 -0
- metadatarr-0.1.0/tests/test_provider_candidates.py +100 -0
- metadatarr-0.1.0/tests/test_providers_registry.py +59 -0
- metadatarr-0.1.0/tests/test_pyfanedit.py +159 -0
- metadatarr-0.1.0/tests/test_resolve_base.py +136 -0
- metadatarr-0.1.0/tests/test_search.py +122 -0
- metadatarr-0.1.0/tests/test_sidecar.py +88 -0
- metadatarr-0.1.0/tests/test_signals.py +107 -0
- metadatarr-0.1.0/tests/test_streams.py +200 -0
metadatarr-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/metadatarr/)
|
|
66
|
+
[](https://pypi.org/project/metadatarr/)
|
|
67
|
+
[](LICENSE)
|
|
68
|
+
[](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).
|