Skip to content

Commit f461651

Browse files
authored
Minor improvements to deezer plugin typing. (#5814)
Added some more typehints to deezer plugin. I know, it is properly not used much and we don't even have test for the deezer plugin but I want to make this a bit more maintainable, mainly to prepare for #5787 and make migration a bit easier.
2 parents a01e603 + 50604b0 commit f461651

File tree

2 files changed

+82
-84
lines changed

2 files changed

+82
-84
lines changed

beets/plugins.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
Any,
2929
Callable,
3030
Generic,
31+
Literal,
3132
Sequence,
3233
TypedDict,
3334
TypeVar,
@@ -737,8 +738,8 @@ def track_url(self) -> str:
737738
@abc.abstractmethod
738739
def _search_api(
739740
self,
740-
query_type: str,
741-
filters: dict[str, str] | None,
741+
query_type: Literal["album", "track"],
742+
filters: dict[str, str],
742743
keywords: str = "",
743744
) -> Sequence[R]:
744745
raise NotImplementedError

beetsplug/deezer.py

Lines changed: 79 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -18,61 +18,49 @@
1818

1919
import collections
2020
import time
21+
from typing import TYPE_CHECKING, Literal, Sequence
2122

2223
import requests
2324
import unidecode
2425

2526
from beets import ui
2627
from beets.autotag import AlbumInfo, TrackInfo
2728
from beets.dbcore import types
28-
from beets.plugins import BeetsPlugin, MetadataSourcePlugin
29+
from beets.plugins import BeetsPlugin, MetadataSourcePlugin, Response
2930

31+
if TYPE_CHECKING:
32+
from beets.library import Item, Library
33+
from beetsplug._typing import JSONDict
3034

31-
class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin):
35+
36+
class DeezerPlugin(MetadataSourcePlugin[Response], BeetsPlugin):
3237
data_source = "Deezer"
3338

3439
item_types = {
3540
"deezer_track_rank": types.INTEGER,
3641
"deezer_track_id": types.INTEGER,
3742
"deezer_updated": types.DATE,
3843
}
39-
4044
# Base URLs for the Deezer API
4145
# Documentation: https://developers.deezer.com/api/
4246
search_url = "https://api.deezer.com/search/"
4347
album_url = "https://api.deezer.com/album/"
4448
track_url = "https://api.deezer.com/track/"
4549

46-
def __init__(self):
47-
super().__init__()
48-
4950
def commands(self):
5051
"""Add beet UI commands to interact with Deezer."""
5152
deezer_update_cmd = ui.Subcommand(
5253
"deezerupdate", help=f"Update {self.data_source} rank"
5354
)
5455

55-
def func(lib, opts, args):
56+
def func(lib: Library, opts, args):
5657
items = lib.items(ui.decargs(args))
57-
self.deezerupdate(items, ui.should_write())
58+
self.deezerupdate(list(items), ui.should_write())
5859

5960
deezer_update_cmd.func = func
6061

6162
return [deezer_update_cmd]
6263

63-
def fetch_data(self, url):
64-
try:
65-
response = requests.get(url, timeout=10)
66-
response.raise_for_status()
67-
data = response.json()
68-
except requests.exceptions.RequestException as e:
69-
self._log.error("Error fetching data from {}\n Error: {}", url, e)
70-
return None
71-
if "error" in data:
72-
self._log.debug("Deezer API error: {}", data["error"]["message"])
73-
return None
74-
return data
75-
7664
def album_for_id(self, album_id: str) -> AlbumInfo | None:
7765
"""Fetch an album by its Deezer ID or URL."""
7866
if not (deezer_id := self._get_id(album_id)):
@@ -156,52 +144,18 @@ def album_for_id(self, album_id: str) -> AlbumInfo | None:
156144
cover_art_url=album_data.get("cover_xl"),
157145
)
158146

159-
def _get_track(self, track_data):
160-
"""Convert a Deezer track object dict to a TrackInfo object.
147+
def track_for_id(self, track_id: str) -> None | TrackInfo:
148+
"""Fetch a track by its Deezer ID or URL.
161149
162-
:param track_data: Deezer Track object dict
163-
:type track_data: dict
164-
:return: TrackInfo object for track
165-
:rtype: beets.autotag.hooks.TrackInfo
150+
Returns a TrackInfo object or None if the track is not found.
166151
"""
167-
artist, artist_id = self.get_artist(
168-
track_data.get("contributors", [track_data["artist"]])
169-
)
170-
return TrackInfo(
171-
title=track_data["title"],
172-
track_id=track_data["id"],
173-
deezer_track_id=track_data["id"],
174-
isrc=track_data.get("isrc"),
175-
artist=artist,
176-
artist_id=artist_id,
177-
length=track_data["duration"],
178-
index=track_data.get("track_position"),
179-
medium=track_data.get("disk_number"),
180-
deezer_track_rank=track_data.get("rank"),
181-
medium_index=track_data.get("track_position"),
182-
data_source=self.data_source,
183-
data_url=track_data["link"],
184-
deezer_updated=time.time(),
185-
)
186-
187-
def track_for_id(self, track_id=None, track_data=None):
188-
"""Fetch a track by its Deezer ID or URL and return a
189-
TrackInfo object or None if the track is not found.
152+
if not (deezer_id := self._get_id(track_id)):
153+
self._log.debug("Invalid Deezer track_id: {}", track_id)
154+
return None
190155

191-
:param track_id: (Optional) Deezer ID or URL for the track. Either
192-
``track_id`` or ``track_data`` must be provided.
193-
:type track_id: str
194-
:param track_data: (Optional) Simplified track object dict. May be
195-
provided instead of ``track_id`` to avoid unnecessary API calls.
196-
:type track_data: dict
197-
:return: TrackInfo object for track
198-
:rtype: beets.autotag.hooks.TrackInfo or None
199-
"""
200-
if track_data is None:
201-
if not (deezer_id := self._get_id(track_id)) or not (
202-
track_data := self.fetch_data(f"{self.track_url}{deezer_id}")
203-
):
204-
return None
156+
if not (track_data := self.fetch_data(f"{self.track_url}{deezer_id}")):
157+
self._log.debug("Track not found: {}", track_id)
158+
return None
205159

206160
track = self._get_track(track_data)
207161

@@ -229,18 +183,43 @@ def track_for_id(self, track_id=None, track_data=None):
229183
track.medium_total = medium_total
230184
return track
231185

186+
def _get_track(self, track_data: JSONDict) -> TrackInfo:
187+
"""Convert a Deezer track object dict to a TrackInfo object.
188+
189+
:param track_data: Deezer Track object dict
190+
:return: TrackInfo object for track
191+
"""
192+
artist, artist_id = self.get_artist(
193+
track_data.get("contributors", [track_data["artist"]])
194+
)
195+
return TrackInfo(
196+
title=track_data["title"],
197+
track_id=track_data["id"],
198+
deezer_track_id=track_data["id"],
199+
isrc=track_data.get("isrc"),
200+
artist=artist,
201+
artist_id=artist_id,
202+
length=track_data["duration"],
203+
index=track_data.get("track_position"),
204+
medium=track_data.get("disk_number"),
205+
deezer_track_rank=track_data.get("rank"),
206+
medium_index=track_data.get("track_position"),
207+
data_source=self.data_source,
208+
data_url=track_data["link"],
209+
deezer_updated=time.time(),
210+
)
211+
232212
@staticmethod
233-
def _construct_search_query(filters=None, keywords=""):
213+
def _construct_search_query(
214+
filters: dict[str, str], keywords: str = ""
215+
) -> str:
234216
"""Construct a query string with the specified filters and keywords to
235217
be provided to the Deezer Search API
236218
(https://developers.deezer.com/api/search).
237219
238-
:param filters: (Optional) Field filters to apply.
239-
:type filters: dict
220+
:param filters: Field filters to apply.
240221
:param keywords: (Optional) Query keywords to use.
241-
:type keywords: str
242222
:return: Query string to be provided to the Search API.
243-
:rtype: str
244223
"""
245224
query_components = [
246225
keywords,
@@ -251,25 +230,30 @@ def _construct_search_query(filters=None, keywords=""):
251230
query = query.decode("utf8")
252231
return unidecode.unidecode(query)
253232

254-
def _search_api(self, query_type, filters=None, keywords=""):
233+
def _search_api(
234+
self,
235+
query_type: Literal[
236+
"album",
237+
"track",
238+
"artist",
239+
"history",
240+
"playlist",
241+
"podcast",
242+
"radio",
243+
"user",
244+
],
245+
filters: dict[str, str],
246+
keywords="",
247+
) -> Sequence[Response]:
255248
"""Query the Deezer Search API for the specified ``keywords``, applying
256249
the provided ``filters``.
257250
258-
:param query_type: The Deezer Search API method to use. Valid types
259-
are: 'album', 'artist', 'history', 'playlist', 'podcast',
260-
'radio', 'track', 'user', and 'track'.
261-
:type query_type: str
262-
:param filters: (Optional) Field filters to apply.
263-
:type filters: dict
251+
:param query_type: The Deezer Search API method to use.
264252
:param keywords: (Optional) Query keywords to use.
265-
:type keywords: str
266253
:return: JSON data for the class:`Response <Response>` object or None
267254
if no search results are returned.
268-
:rtype: dict or None
269255
"""
270256
query = self._construct_search_query(keywords=keywords, filters=filters)
271-
if not query:
272-
return None
273257
self._log.debug(f"Searching {self.data_source} for '{query}'")
274258
try:
275259
response = requests.get(
@@ -284,7 +268,7 @@ def _search_api(self, query_type, filters=None, keywords=""):
284268
self.data_source,
285269
e,
286270
)
287-
return None
271+
return ()
288272
response_data = response.json().get("data", [])
289273
self._log.debug(
290274
"Found {} result(s) from {} for '{}'",
@@ -294,7 +278,7 @@ def _search_api(self, query_type, filters=None, keywords=""):
294278
)
295279
return response_data
296280

297-
def deezerupdate(self, items, write):
281+
def deezerupdate(self, items: Sequence[Item], write: bool):
298282
"""Obtain rank information from Deezer."""
299283
for index, item in enumerate(items, start=1):
300284
self._log.info(
@@ -320,3 +304,16 @@ def deezerupdate(self, items, write):
320304
item.deezer_updated = time.time()
321305
if write:
322306
item.try_write()
307+
308+
def fetch_data(self, url: str):
309+
try:
310+
response = requests.get(url, timeout=10)
311+
response.raise_for_status()
312+
data = response.json()
313+
except requests.exceptions.RequestException as e:
314+
self._log.error("Error fetching data from {}\n Error: {}", url, e)
315+
return None
316+
if "error" in data:
317+
self._log.debug("Deezer API error: {}", data["error"]["message"])
318+
return None
319+
return data

0 commit comments

Comments
 (0)