selin-vinyl-api 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.
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quantum Protection Security Services LLC (Selin)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: selin-vinyl-api
3
+ Version: 0.1.0
4
+ Summary: Official Python client for the Selin Vinyl API: music metadata, matching, credits and cover art.
5
+ License: MIT
6
+ Project-URL: Homepage, https://selin-ai.com/vinyl-api
7
+ Project-URL: Documentation, https://vinyl-api.selin-ai.com/docs
8
+ Keywords: music,metadata,musicbrainz,isrc,upc,api,selin,vinyl
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Typing :: Typed
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # selin-vinyl-api (Python)
18
+
19
+ Official client for the [Selin Vinyl API](https://vinyl-api.selin-ai.com/docs): artists, albums, tracklists, ISRC and barcode lookups, credits, cover art and track matching, built on MusicBrainz and Wikidata (CC0) plus Selin's own listening data.
20
+
21
+ No dependencies (standard library only). Python 3.9+.
22
+
23
+ ```sh
24
+ pip install selin-vinyl-api
25
+ ```
26
+
27
+ Get a free key at <https://selin-ai.com/vinyl-api>.
28
+
29
+ ```python
30
+ from selin_vinyl import VinylAPI, VinylAPIError
31
+
32
+ vinyl = VinylAPI(api_key="vk_...") # or set VINYL_API_KEY
33
+
34
+ artists = vinyl.search_artists("prince", limit=3)["artists"]
35
+ album = vinyl.get_album_tracks("<release-group id>", credits=True)
36
+
37
+ # Which recording is this? The matching tool for library imports.
38
+ match = vinyl.match_track("Purple Rain", artist="Prince", duration_ms=520000)["match"]
39
+
40
+ # Up to 50 lookups in one request.
41
+ results = vinyl.batch([
42
+ {"id": "a", "type": "isrc", "value": "USWB11602075"},
43
+ {"id": "b", "type": "upc", "value": "602547924139"},
44
+ ])["results"]
45
+ ```
46
+
47
+ Methods are the API's operations in snake_case: `search_artists`, `get_artist`, `list_artist_releases`, `list_artist_credits`, `lookup_artist`, `search_albums`, `get_album`, `get_album_tracks`, `get_album_artwork`, `lookup_album`, `list_new_releases`, `lookup_upc`, `search_tracks`, `lookup_isrc`, `get_recording`, `match_track`, `batch`, `get_artist_chart`, `get_usage`, `get_plans`. Each returns the parsed JSON; the [API reference](https://vinyl-api.selin-ai.com/docs) documents every field.
48
+
49
+ ## Errors, limits and retries
50
+
51
+ ```python
52
+ try:
53
+ vinyl.get_artist(artist_id)
54
+ except VinylAPIError as e:
55
+ print(e.status, e.code, e.retry_after)
56
+
57
+ vinyl.rate_limit # {"limit", "remaining", "quota_limit", "quota_used"} from the last response
58
+ ```
59
+
60
+ A `429` is retried automatically after the `Retry-After` wait (up to `max_retries`, default 2); gateway errors on GETs are retried with backoff. Constructor options: `base_url`, `max_retries`, `timeout`.
61
+
62
+ ## Attribution
63
+
64
+ Show the `attribution` string the API returns wherever you display its data. Wikipedia biographies are CC BY-SA 4.0 (credit and link back); cover art and artist photos belong to their rights holders.
65
+
66
+ MIT licensed.
@@ -0,0 +1,50 @@
1
+ # selin-vinyl-api (Python)
2
+
3
+ Official client for the [Selin Vinyl API](https://vinyl-api.selin-ai.com/docs): artists, albums, tracklists, ISRC and barcode lookups, credits, cover art and track matching, built on MusicBrainz and Wikidata (CC0) plus Selin's own listening data.
4
+
5
+ No dependencies (standard library only). Python 3.9+.
6
+
7
+ ```sh
8
+ pip install selin-vinyl-api
9
+ ```
10
+
11
+ Get a free key at <https://selin-ai.com/vinyl-api>.
12
+
13
+ ```python
14
+ from selin_vinyl import VinylAPI, VinylAPIError
15
+
16
+ vinyl = VinylAPI(api_key="vk_...") # or set VINYL_API_KEY
17
+
18
+ artists = vinyl.search_artists("prince", limit=3)["artists"]
19
+ album = vinyl.get_album_tracks("<release-group id>", credits=True)
20
+
21
+ # Which recording is this? The matching tool for library imports.
22
+ match = vinyl.match_track("Purple Rain", artist="Prince", duration_ms=520000)["match"]
23
+
24
+ # Up to 50 lookups in one request.
25
+ results = vinyl.batch([
26
+ {"id": "a", "type": "isrc", "value": "USWB11602075"},
27
+ {"id": "b", "type": "upc", "value": "602547924139"},
28
+ ])["results"]
29
+ ```
30
+
31
+ Methods are the API's operations in snake_case: `search_artists`, `get_artist`, `list_artist_releases`, `list_artist_credits`, `lookup_artist`, `search_albums`, `get_album`, `get_album_tracks`, `get_album_artwork`, `lookup_album`, `list_new_releases`, `lookup_upc`, `search_tracks`, `lookup_isrc`, `get_recording`, `match_track`, `batch`, `get_artist_chart`, `get_usage`, `get_plans`. Each returns the parsed JSON; the [API reference](https://vinyl-api.selin-ai.com/docs) documents every field.
32
+
33
+ ## Errors, limits and retries
34
+
35
+ ```python
36
+ try:
37
+ vinyl.get_artist(artist_id)
38
+ except VinylAPIError as e:
39
+ print(e.status, e.code, e.retry_after)
40
+
41
+ vinyl.rate_limit # {"limit", "remaining", "quota_limit", "quota_used"} from the last response
42
+ ```
43
+
44
+ A `429` is retried automatically after the `Retry-After` wait (up to `max_retries`, default 2); gateway errors on GETs are retried with backoff. Constructor options: `base_url`, `max_retries`, `timeout`.
45
+
46
+ ## Attribution
47
+
48
+ Show the `attribution` string the API returns wherever you display its data. Wikipedia biographies are CC BY-SA 4.0 (credit and link back); cover art and artist photos belong to their rights holders.
49
+
50
+ MIT licensed.
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "selin-vinyl-api"
7
+ version = "0.1.0"
8
+ description = "Official Python client for the Selin Vinyl API: music metadata, matching, credits and cover art."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ keywords = ["music", "metadata", "musicbrainz", "isrc", "upc", "api", "selin", "vinyl"]
13
+ classifiers = ["Programming Language :: Python :: 3", "Typing :: Typed", "License :: OSI Approved :: MIT License"]
14
+ dependencies = []
15
+
16
+ [project.urls]
17
+ Homepage = "https://selin-ai.com/vinyl-api"
18
+ Documentation = "https://vinyl-api.selin-ai.com/docs"
19
+
20
+ [tool.setuptools]
21
+ packages = ["selin_vinyl"]
22
+ [tool.setuptools.package-data]
23
+ selin_vinyl = ["py.typed"]
@@ -0,0 +1,7 @@
1
+ """Selin Vinyl API client."""
2
+ __version__ = "0.1.0"
3
+
4
+ from ._transport import VinylAPIError # noqa: E402
5
+ from .client import VinylAPI # noqa: E402
6
+
7
+ __all__ = ["VinylAPI", "VinylAPIError", "__version__"]
@@ -0,0 +1,85 @@
1
+ import json
2
+ import os
3
+ import time
4
+ import urllib.error
5
+ import urllib.parse
6
+ import urllib.request
7
+ from typing import Any, Dict, Optional
8
+
9
+ from . import __version__
10
+
11
+
12
+ class VinylAPIError(Exception):
13
+ """The API answered with an error status."""
14
+
15
+ def __init__(self, status: int, code: str, body: Any = None, retry_after: Optional[float] = None):
16
+ super().__init__(f"Vinyl API {status}: {code}")
17
+ self.status = status
18
+ self.code = code # the API's error message or code, e.g. "not_found"
19
+ self.body = body
20
+ self.retry_after = retry_after # seconds to wait before retrying, on a 429
21
+
22
+
23
+ def _num(v: Optional[str]) -> Optional[int]:
24
+ try:
25
+ return int(v) if v not in (None, "") else None
26
+ except ValueError:
27
+ return None
28
+
29
+
30
+ class Transport:
31
+ def __init__(self, api_key: Optional[str] = None, *, base_url: Optional[str] = None, max_retries: int = 2, timeout: float = 30.0):
32
+ self.api_key = api_key or os.environ.get("VINYL_API_KEY")
33
+ self.base_url = (base_url or os.environ.get("VINYL_API_URL") or "https://vinyl-api.selin-ai.com").rstrip("/")
34
+ self.max_retries = max_retries
35
+ self.timeout = timeout
36
+ #: Rate-limit and quota numbers from the most recent response.
37
+ self.rate_limit: Dict[str, Optional[int]] = {}
38
+
39
+ def _request(self, method: str, route: str, *, query: Dict[str, Any], body: Any, auth: bool, timeout: Optional[float]) -> Any:
40
+ if auth and not self.api_key:
41
+ raise ValueError("No API key: pass api_key=... or set VINYL_API_KEY. Get a free key at https://selin-ai.com/vinyl-api")
42
+ qs = urllib.parse.urlencode({k: ("true" if v is True else "false" if v is False else v) for k, v in query.items() if v is not None})
43
+ url = self.base_url + route + ("?" + qs if qs else "")
44
+ headers = {"accept": "application/json", "user-agent": f"selin-vinyl-api-python/{__version__}"}
45
+ if self.api_key:
46
+ headers["x-api-key"] = self.api_key
47
+ data = None
48
+ if body is not None:
49
+ data = json.dumps(body).encode()
50
+ headers["content-type"] = "application/json"
51
+
52
+ attempt = 0
53
+ while True:
54
+ req = urllib.request.Request(url, data=data, method=method, headers=headers)
55
+ try:
56
+ with urllib.request.urlopen(req, timeout=timeout or self.timeout) as res:
57
+ self._remember(res.headers)
58
+ return json.loads(res.read().decode())
59
+ except urllib.error.HTTPError as e:
60
+ self._remember(e.headers)
61
+ text = e.read().decode(errors="replace")
62
+ try:
63
+ parsed: Any = json.loads(text)
64
+ except ValueError:
65
+ parsed = text
66
+ retry_after = _num(e.headers.get("retry-after"))
67
+ code = parsed.get("error") if isinstance(parsed, dict) and parsed.get("error") else e.reason
68
+ transient = (retry_after or 1) <= 30 if e.code == 429 else (method == "GET" and e.code in (502, 503, 504))
69
+ if attempt < self.max_retries and transient:
70
+ time.sleep((retry_after or 1) if e.code == 429 else 0.4 * 2 ** attempt)
71
+ attempt += 1
72
+ continue
73
+ raise VinylAPIError(e.code, str(code), parsed, retry_after) from None
74
+ except (urllib.error.URLError, TimeoutError):
75
+ if attempt < self.max_retries and method == "GET":
76
+ time.sleep(0.4 * 2 ** attempt)
77
+ attempt += 1
78
+ continue
79
+ raise
80
+
81
+ def _remember(self, h: Any) -> None:
82
+ self.rate_limit = {
83
+ "limit": _num(h.get("x-ratelimit-limit")), "remaining": _num(h.get("x-ratelimit-remaining")),
84
+ "quota_limit": _num(h.get("x-quota-limit")), "quota_used": _num(h.get("x-quota-used")),
85
+ }
@@ -0,0 +1,348 @@
1
+ # GENERATED by sdk/generate.js from the OpenAPI description. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Optional
5
+ from urllib.parse import quote
6
+
7
+ from ._transport import Transport
8
+
9
+
10
+ class VinylAPI(Transport):
11
+ """Client for the Selin Vinyl API (https://vinyl-api.selin-ai.com/docs).
12
+
13
+ >>> vinyl = VinylAPI(api_key="vk_...") # or set VINYL_API_KEY
14
+ >>> vinyl.search_artists("prince", limit=3)["artists"][0]["name"]
15
+ 'Prince'
16
+ """
17
+
18
+ def search_artists(self, q: str, limit: Optional[int] = None, *, timeout: Optional[float] = None) -> Any:
19
+ """Search artists.
20
+
21
+ Best match first: exact name, then prefix, then fuzzy, preferring artists with more releases.
22
+
23
+ ``GET /v1/artists/search``
24
+
25
+ Args:
26
+ q: Artist name (2+ characters).
27
+ limit: How many results to return (1-25).
28
+ """
29
+ return self._request("GET", "/v1/artists/search",
30
+ query={"q": q, "limit": limit},
31
+ body=None, auth=True, timeout=timeout)
32
+
33
+ def get_artist(self, id: str, *, timeout: Optional[float] = None) -> Any:
34
+ """Get an artist.
35
+
36
+ Profile, members, release counts, genres, listening stats and Wikipedia/Wikidata enrichment.
37
+
38
+ ``GET /v1/artists/{id}``
39
+
40
+ Args:
41
+ id: The artist's MusicBrainz id (UUID).
42
+ """
43
+ return self._request("GET", "/v1/artists/{id}".format(id=quote(str(id), safe="")),
44
+ query={},
45
+ body=None, auth=True, timeout=timeout)
46
+
47
+ def list_artist_releases(self, id: str, type: Optional[str] = None, studio: Optional[bool] = None, limit: Optional[int] = None, offset: Optional[int] = None, *, timeout: Optional[float] = None) -> Any:
48
+ """List an artist's releases.
49
+
50
+ Albums, EPs and singles, newest first.
51
+
52
+ ``GET /v1/artists/{id}/releases``
53
+
54
+ Args:
55
+ id: The artist's MusicBrainz id (UUID).
56
+ type: Album, EP or single (default: all).
57
+ studio: Hide live, compilation, remix and other secondary types (`studio=true`; `1` also works).
58
+ limit: How many results to return (1-200).
59
+ offset: How many results to skip, for paging.
60
+ """
61
+ return self._request("GET", "/v1/artists/{id}/releases".format(id=quote(str(id), safe="")),
62
+ query={"type": type, "studio": studio, "limit": limit, "offset": offset},
63
+ body=None, auth=True, timeout=timeout)
64
+
65
+ def list_artist_credits(self, id: str, role: Optional[str] = None, limit: Optional[int] = None, offset: Optional[int] = None, *, timeout: Optional[float] = None) -> Any:
66
+ """Recordings an artist is credited on.
67
+
68
+ Producer, engineer, mix, mastering, arranger, remixer, performer and other credited roles.
69
+
70
+ ``GET /v1/artists/{id}/credits``
71
+
72
+ Args:
73
+ id: The artist's MusicBrainz id (UUID).
74
+ role: Only this role.
75
+ limit: How many results to return (1-100).
76
+ offset: How many results to skip, for paging.
77
+ """
78
+ return self._request("GET", "/v1/artists/{id}/credits".format(id=quote(str(id), safe="")),
79
+ query={"role": role, "limit": limit, "offset": offset},
80
+ body=None, auth=True, timeout=timeout)
81
+
82
+ def lookup_artist(self, spotify: Optional[str] = None, apple: Optional[str] = None, deezer: Optional[str] = None, *, timeout: Optional[float] = None) -> Any:
83
+ """Find an artist by another service's id.
84
+
85
+ Cross-reference from a Spotify, Apple Music or Deezer artist id to the MusicBrainz artist.
86
+
87
+ ``GET /v1/lookup/artist``
88
+
89
+ Args:
90
+ spotify: Spotify artist id.
91
+ apple: Apple Music artist id.
92
+ deezer: Deezer artist id.
93
+ """
94
+ return self._request("GET", "/v1/lookup/artist",
95
+ query={"spotify": spotify, "apple": apple, "deezer": deezer},
96
+ body=None, auth=True, timeout=timeout)
97
+
98
+ def search_albums(self, q: str, artist: Optional[str] = None, type: Optional[str] = None, limit: Optional[int] = None, offset: Optional[int] = None, *, timeout: Optional[float] = None) -> Any:
99
+ """Search albums, EPs and singles.
100
+
101
+ Ranks exact titles, studio releases and releases with a tracklist first, then popularity.
102
+
103
+ ``GET /v1/release-groups/search``
104
+
105
+ Args:
106
+ q: Title (2+ characters).
107
+ artist: Narrow to an artist: a MusicBrainz id or part of the name.
108
+ type: Only this kind of release.
109
+ limit: How many results to return (1-25).
110
+ offset: How many results to skip, for paging.
111
+ """
112
+ return self._request("GET", "/v1/release-groups/search",
113
+ query={"q": q, "artist": artist, "type": type, "limit": limit, "offset": offset},
114
+ body=None, auth=True, timeout=timeout)
115
+
116
+ def get_album(self, id: str, *, timeout: Optional[float] = None) -> Any:
117
+ """Get a release group.
118
+
119
+ One album, EP or single with its credited artists.
120
+
121
+ ``GET /v1/release-groups/{id}``
122
+
123
+ Args:
124
+ id: The release group's MusicBrainz id (UUID).
125
+ """
126
+ return self._request("GET", "/v1/release-groups/{id}".format(id=quote(str(id), safe="")),
127
+ query={},
128
+ body=None, auth=True, timeout=timeout)
129
+
130
+ def get_album_tracks(self, id: str, credits: Optional[bool] = None, *, timeout: Optional[float] = None) -> Any:
131
+ """Get an album's tracklist.
132
+
133
+ The tracklist of the representative release (the earliest official edition), with recording ids and ISRCs.
134
+
135
+ ``GET /v1/release-groups/{id}/tracks``
136
+
137
+ Args:
138
+ id: The release group's MusicBrainz id (UUID).
139
+ credits: Attach credits to every track (`credits=true`; `1` also works).
140
+ """
141
+ return self._request("GET", "/v1/release-groups/{id}/tracks".format(id=quote(str(id), safe="")),
142
+ query={"credits": credits},
143
+ body=None, auth=True, timeout=timeout)
144
+
145
+ def get_album_artwork(self, id: str, *, timeout: Optional[float] = None) -> Any:
146
+ """Get cover art.
147
+
148
+ Cover art links from the Cover Art Archive. Verifies the image exists (cached 30 days).
149
+
150
+ ``GET /v1/release-groups/{id}/artwork``
151
+
152
+ Args:
153
+ id: The release group's MusicBrainz id (UUID).
154
+ """
155
+ return self._request("GET", "/v1/release-groups/{id}/artwork".format(id=quote(str(id), safe="")),
156
+ query={},
157
+ body=None, auth=True, timeout=timeout)
158
+
159
+ def lookup_album(self, artist: str, title: str, year: Optional[int] = None, *, timeout: Optional[float] = None) -> Any:
160
+ """Find an artist's album by title.
161
+
162
+ Punctuation and case insensitive; deluxe/remaster suffixes ignored; prefers the year given. Only groups with a tracklist.
163
+
164
+ ``GET /v1/lookup/album``
165
+
166
+ Args:
167
+ artist: The artist's MusicBrainz id.
168
+ title: Album title.
169
+ year: Preferred release year.
170
+ """
171
+ return self._request("GET", "/v1/lookup/album",
172
+ query={"artist": artist, "title": title, "year": year},
173
+ body=None, auth=True, timeout=timeout)
174
+
175
+ def list_new_releases(self, days: Optional[int] = None, type: Optional[str] = None, all: Optional[bool] = None, limit: Optional[int] = None, offset: Optional[int] = None, *, timeout: Optional[float] = None) -> Any:
176
+ """New releases.
177
+
178
+ The latest albums, EPs and singles in the catalogue, newest first.
179
+
180
+ ``GET /v1/releases/new``
181
+
182
+ Args:
183
+ days: Look back this many days (1-90).
184
+ type: Only this kind of release.
185
+ all: Include live, compilation, remix and other secondary types (`all=true`; `1` also works).
186
+ limit: How many results to return (1-100).
187
+ offset: How many results to skip, for paging.
188
+ """
189
+ return self._request("GET", "/v1/releases/new",
190
+ query={"days": days, "type": type, "all": all, "limit": limit, "offset": offset},
191
+ body=None, auth=True, timeout=timeout)
192
+
193
+ def lookup_upc(self, code: str, *, timeout: Optional[float] = None) -> Any:
194
+ """Look up a release by barcode.
195
+
196
+ UPC-A, EAN-13 and GTIN-14 spellings of the same barcode all work.
197
+
198
+ ``GET /v1/lookup/upc/{code}``
199
+
200
+ Args:
201
+ code: The barcode (8-14 digits).
202
+ """
203
+ return self._request("GET", "/v1/lookup/upc/{code}".format(code=quote(str(code), safe="")),
204
+ query={},
205
+ body=None, auth=True, timeout=timeout)
206
+
207
+ def search_tracks(self, q: str, artist: Optional[str] = None, limit: Optional[int] = None, offset: Optional[int] = None, *, timeout: Optional[float] = None) -> Any:
208
+ """Search tracks.
209
+
210
+ Find recordings by title, optionally narrowed by artist. Common titles are ranked by artist popularity.
211
+
212
+ ``GET /v1/tracks/search``
213
+
214
+ Args:
215
+ q: Title (2+ characters).
216
+ artist: Artist name to narrow the search.
217
+ limit: How many results to return (1-25).
218
+ offset: How many results to skip, for paging.
219
+ """
220
+ return self._request("GET", "/v1/tracks/search",
221
+ query={"q": q, "artist": artist, "limit": limit, "offset": offset},
222
+ body=None, auth=True, timeout=timeout)
223
+
224
+ def lookup_isrc(self, isrc: str, *, timeout: Optional[float] = None) -> Any:
225
+ """Look up a track by ISRC.
226
+
227
+ The recording and every album, EP or single we hold it on.
228
+
229
+ ``GET /v1/tracks/isrc/{isrc}``
230
+
231
+ Args:
232
+ isrc: A 12-character ISRC (hyphens ignored).
233
+ """
234
+ return self._request("GET", "/v1/tracks/isrc/{isrc}".format(isrc=quote(str(isrc), safe="")),
235
+ query={},
236
+ body=None, auth=True, timeout=timeout)
237
+
238
+ def get_recording(self, id: str, *, timeout: Optional[float] = None) -> Any:
239
+ """Get a recording.
240
+
241
+ A recording with its ISRCs, the releases it appears on and its credits.
242
+
243
+ ``GET /v1/recordings/{id}``
244
+
245
+ Args:
246
+ id: The recording's MusicBrainz id (UUID).
247
+ """
248
+ return self._request("GET", "/v1/recordings/{id}".format(id=quote(str(id), safe="")),
249
+ query={},
250
+ body=None, auth=True, timeout=timeout)
251
+
252
+ def match_track(self, title: str, artist: Optional[str] = None, album: Optional[str] = None, duration_ms: Optional[int] = None, duration: Optional[float] = None, *, timeout: Optional[float] = None) -> Any:
253
+ """Match a track.
254
+
255
+ Identify the recording behind a title, artist, album and duration: the matching tool for library imports and playlists. "(Remastered)" style suffixes are ignored. Weights: title 55%, artist 30%, album 10%, duration 5%.
256
+
257
+ ``GET /v1/match``
258
+
259
+ Args:
260
+ title: Track title.
261
+ artist: Artist name.
262
+ album: Album name.
263
+ duration_ms: Duration in milliseconds.
264
+ duration: Duration in seconds (alternative).
265
+ """
266
+ return self._request("GET", "/v1/match",
267
+ query={"title": title, "artist": artist, "album": album, "durationMs": duration_ms, "duration": duration},
268
+ body=None, auth=True, timeout=timeout)
269
+
270
+ def batch(self, requests: list, *, timeout: Optional[float] = None) -> Any:
271
+ """Batch lookups.
272
+
273
+ Up to 50 ISRC, UPC, match and artist-lookup requests in one call. Each lookup counts as one request against your plan. Results come back in request order; a lookup that fails has an `error` instead of a `result`.
274
+
275
+ ``POST /v1/batch``
276
+
277
+ Args:
278
+ requests: Up to 50 lookups. Each counts as one request against your plan.
279
+ """
280
+ return self._request("POST", "/v1/batch",
281
+ query={},
282
+ body={"requests": requests}, auth=True, timeout=timeout)
283
+
284
+ def get_artist_chart(self, days: Optional[int] = None, limit: Optional[int] = None, *, timeout: Optional[float] = None) -> Any:
285
+ """Artist chart.
286
+
287
+ Artists ranked by distinct listeners on Selin (then plays) over the last N days.
288
+
289
+ ``GET /v1/charts/artists``
290
+
291
+ Args:
292
+ days: Window in days (1-90).
293
+ limit: How many results to return (1-100).
294
+ """
295
+ return self._request("GET", "/v1/charts/artists",
296
+ query={"days": days, "limit": limit},
297
+ body=None, auth=True, timeout=timeout)
298
+
299
+ def get_usage(self, *, timeout: Optional[float] = None) -> Any:
300
+ """Your usage.
301
+
302
+ This month's request count and the last 30 days by endpoint, for the key making the call.
303
+
304
+ ``GET /v1/usage``
305
+ """
306
+ return self._request("GET", "/v1/usage",
307
+ query={},
308
+ body=None, auth=True, timeout=timeout)
309
+
310
+ def get_plans(self, *, timeout: Optional[float] = None) -> Any:
311
+ """Plans and prices.
312
+
313
+ No key needed.
314
+
315
+ ``GET /portal/config``
316
+ """
317
+ return self._request("GET", "/portal/config",
318
+ query={},
319
+ body=None, auth=False, timeout=timeout)
320
+
321
+ def signup(self, name: str, email: str, *, timeout: Optional[float] = None) -> Any:
322
+ """Get a free key.
323
+
324
+ Creates a free key (one per email, a few per network per day). The key is shown once.
325
+
326
+ ``POST /portal/signup``
327
+
328
+ Args:
329
+ name: Your name or company.
330
+ email: Your email.
331
+ """
332
+ return self._request("POST", "/portal/signup",
333
+ query={},
334
+ body={"name": name, "email": email}, auth=False, timeout=timeout)
335
+
336
+ def start_checkout(self, tier: str, *, timeout: Optional[float] = None) -> Any:
337
+ """Upgrade your plan.
338
+
339
+ Starts a Stripe Checkout for a paid plan; authenticated with your own key. Your key upgrades automatically once payment completes.
340
+
341
+ ``POST /portal/checkout``
342
+
343
+ Args:
344
+ tier: The plan to buy.
345
+ """
346
+ return self._request("POST", "/portal/checkout",
347
+ query={},
348
+ body={"tier": tier}, auth=True, timeout=timeout)
File without changes
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: selin-vinyl-api
3
+ Version: 0.1.0
4
+ Summary: Official Python client for the Selin Vinyl API: music metadata, matching, credits and cover art.
5
+ License: MIT
6
+ Project-URL: Homepage, https://selin-ai.com/vinyl-api
7
+ Project-URL: Documentation, https://vinyl-api.selin-ai.com/docs
8
+ Keywords: music,metadata,musicbrainz,isrc,upc,api,selin,vinyl
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Typing :: Typed
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # selin-vinyl-api (Python)
18
+
19
+ Official client for the [Selin Vinyl API](https://vinyl-api.selin-ai.com/docs): artists, albums, tracklists, ISRC and barcode lookups, credits, cover art and track matching, built on MusicBrainz and Wikidata (CC0) plus Selin's own listening data.
20
+
21
+ No dependencies (standard library only). Python 3.9+.
22
+
23
+ ```sh
24
+ pip install selin-vinyl-api
25
+ ```
26
+
27
+ Get a free key at <https://selin-ai.com/vinyl-api>.
28
+
29
+ ```python
30
+ from selin_vinyl import VinylAPI, VinylAPIError
31
+
32
+ vinyl = VinylAPI(api_key="vk_...") # or set VINYL_API_KEY
33
+
34
+ artists = vinyl.search_artists("prince", limit=3)["artists"]
35
+ album = vinyl.get_album_tracks("<release-group id>", credits=True)
36
+
37
+ # Which recording is this? The matching tool for library imports.
38
+ match = vinyl.match_track("Purple Rain", artist="Prince", duration_ms=520000)["match"]
39
+
40
+ # Up to 50 lookups in one request.
41
+ results = vinyl.batch([
42
+ {"id": "a", "type": "isrc", "value": "USWB11602075"},
43
+ {"id": "b", "type": "upc", "value": "602547924139"},
44
+ ])["results"]
45
+ ```
46
+
47
+ Methods are the API's operations in snake_case: `search_artists`, `get_artist`, `list_artist_releases`, `list_artist_credits`, `lookup_artist`, `search_albums`, `get_album`, `get_album_tracks`, `get_album_artwork`, `lookup_album`, `list_new_releases`, `lookup_upc`, `search_tracks`, `lookup_isrc`, `get_recording`, `match_track`, `batch`, `get_artist_chart`, `get_usage`, `get_plans`. Each returns the parsed JSON; the [API reference](https://vinyl-api.selin-ai.com/docs) documents every field.
48
+
49
+ ## Errors, limits and retries
50
+
51
+ ```python
52
+ try:
53
+ vinyl.get_artist(artist_id)
54
+ except VinylAPIError as e:
55
+ print(e.status, e.code, e.retry_after)
56
+
57
+ vinyl.rate_limit # {"limit", "remaining", "quota_limit", "quota_used"} from the last response
58
+ ```
59
+
60
+ A `429` is retried automatically after the `Retry-After` wait (up to `max_retries`, default 2); gateway errors on GETs are retried with backoff. Constructor options: `base_url`, `max_retries`, `timeout`.
61
+
62
+ ## Attribution
63
+
64
+ Show the `attribution` string the API returns wherever you display its data. Wikipedia biographies are CC BY-SA 4.0 (credit and link back); cover art and artist photos belong to their rights holders.
65
+
66
+ MIT licensed.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ selin_vinyl/__init__.py
5
+ selin_vinyl/_transport.py
6
+ selin_vinyl/client.py
7
+ selin_vinyl/py.typed
8
+ selin_vinyl_api.egg-info/PKG-INFO
9
+ selin_vinyl_api.egg-info/SOURCES.txt
10
+ selin_vinyl_api.egg-info/dependency_links.txt
11
+ selin_vinyl_api.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ selin_vinyl
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+