From 545f0a3f49a195682ab4fc913ef7f5ac22cb5cb6 Mon Sep 17 00:00:00 2001 From: Serene-Arc <33189705+Serene-Arc@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:03:53 +1000 Subject: [PATCH 1/9] Fix some wrong typings --- beetsplug/api.py | 4 ++-- beetsplug/goodreads.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/beetsplug/api.py b/beetsplug/api.py index 618c351..65ad238 100644 --- a/beetsplug/api.py +++ b/beetsplug/api.py @@ -1,7 +1,7 @@ import json import xml.etree.ElementTree as ET from time import sleep -from typing import Optional +from typing import Optional, AnyStr from urllib import parse, request from urllib.error import HTTPError @@ -71,7 +71,7 @@ def get_audible_album_region(url: str) -> Optional[str]: return None -def make_request(url: str) -> bytes: +def make_request(url: str) -> AnyStr | None: """Makes a request to the specified url and returns received response The request will be retried up to 3 times in case of failure. """ diff --git a/beetsplug/goodreads.py b/beetsplug/goodreads.py index 4e62014..56134d5 100644 --- a/beetsplug/goodreads.py +++ b/beetsplug/goodreads.py @@ -24,7 +24,7 @@ def get_original_date(self, asin: str, authors: str, title: str) -> dict: return original_date -def goodreads_get_best_match(self, response: Element, author: str, title: str) -> Element: +def goodreads_get_best_match(self, response: Element, author: str, title: str) -> Element | None: # returns best matching work from results author_cleaned = author.replace(" ", "") # get all works From efaa1f5c4596165a3c12ad8b387868131c6b536a Mon Sep 17 00:00:00 2001 From: Serene-Arc <33189705+Serene-Arc@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:11:29 +1000 Subject: [PATCH 2/9] Add missing return types --- beetsplug/audible.py | 35 ++++++++++++++++++----------------- beetsplug/book.py | 4 ++-- beetsplug/goodreads.py | 2 +- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/beetsplug/audible.py b/beetsplug/audible.py index 174a330..bbda8e9 100644 --- a/beetsplug/audible.py +++ b/beetsplug/audible.py @@ -5,6 +5,7 @@ import urllib.error from contextlib import suppress from tempfile import NamedTemporaryFile +from typing import Sequence import mediafile import yaml @@ -131,7 +132,7 @@ def __init__(self): self._recent_items = [] - def candidates(self, items, artist, album, va_likely): + def candidates(self, items, artist, album, va_likely) -> list[AlbumInfo]: """Returns a list of AlbumInfo objects for Audible search results matching an album and artist (if not various). """ @@ -202,7 +203,7 @@ def candidates(self, items, artist, album, va_likely): ) return albums - def maybe_align_tracks_with_items(self, album_info, items, *, is_likely_match=True): + def maybe_align_tracks_with_items(self, album_info, items, *, is_likely_match=True) -> int | None: """Override chapter data from Audible with the current file list when needed.""" if not is_likely_match or not items or not album_info.tracks: return None @@ -231,7 +232,7 @@ def maybe_align_tracks_with_items(self, album_info, items, *, is_likely_match=Tr ] return chapter_count_from_audible - def get_album_from_yaml_metadata(self, data, items): + def get_album_from_yaml_metadata(self, data, items) -> AlbumInfo: """Returns an `AlbumInfo` object by populating it with details from metadata.yml""" title = data["title"] subtitle = data.get("subtitle") @@ -312,7 +313,7 @@ def get_album_from_yaml_metadata(self, data, items): **common_attributes, ) - def album_for_id(self, album_id): + def album_for_id(self, album_id) -> AlbumInfo | None: """ Fetches book info by its asin and returns an AlbumInfo object or None if the book was not found. @@ -337,7 +338,7 @@ def album_for_id(self, album_id): self._log.debug(f"Exception while getting book {asin}", exc_info=True) return None - def get_albums(self, query, region): + def get_albums(self, query, region) -> list[AlbumInfo]: """Returns a list of AlbumInfo objects for an Audible search query.""" try: @@ -367,7 +368,7 @@ def get_albums(self, query, region): self._log.warn("Error while fetching book information from Audnex", exc_info=True) return [] - def get_album_info(self, asin, region): + def get_album_info(self, asin, region) -> AlbumInfo: """Returns an AlbumInfo object for a book given its asin.""" (book, chapters) = get_book_info(asin, region) @@ -500,11 +501,11 @@ def get_album_info(self, asin, region): **common_attributes, ) - def track_for_id(self, track_id: str): + def track_for_id(self, track_id: str) -> None: self._log.debug("Searching for track {}", track_id) return None - def item_candidates(self, item, artist, title): + def item_candidates(self, item, artist, title) -> list[TrackInfo]: """Returns a list of TrackInfo objects for individual track search. Audiobooks are not searched by individual tracks, so this returns an empty list. @@ -515,7 +516,7 @@ def item_candidates(self, item, artist, title): return [] @staticmethod - def on_write(item, path, tags): + def on_write(item, path, tags) -> None: # Strip unwanted tags that Beets automatically adds tags["mb_albumid"] = None tags["mb_trackid"] = None @@ -531,7 +532,7 @@ def on_write(item, path, tags): # The "mvi" tag for m4b files only accepts integers tags["mvi"] = int(tags.get("series_position")) - def fetch_art(self, session, task): + def fetch_art(self, session, task) -> None: # Only fetch art for albums if task.is_album: if task.album.artpath and os.path.isfile(task.album.artpath): @@ -556,7 +557,7 @@ def fetch_art(self, session, task): f"Error while downloading cover art for {title} by {author} from {cover_url}", exc_info=True ) - def fetch_image(self, url): + def fetch_image(self, url) -> bytes: """Downloads an image from a URL and returns a path to the downloaded image.""" image = make_request(url) ext = url[-4:] # e.g, ".jpg" @@ -565,14 +566,14 @@ def fetch_image(self, url): self._log.debug("downloaded art to: {0}", util.displayable_path(fh.name)) return util.bytestring_path(fh.name) - def on_import_task_files(self, task, session): + def on_import_task_files(self, task, session) -> None: self.write_book_description_and_narrator(task.imported_items()) if self.config["fetch_art"] and task in self.cover_art: cover_path = self.cover_art.pop(task) task.album.set_art(cover_path, True) task.album.store() - def write_book_description_and_narrator(self, items): + def write_book_description_and_narrator(self, items) -> None: """Write description.txt, reader.txt and cover art""" if len(items) == 0: return @@ -590,17 +591,17 @@ def write_book_description_and_narrator(self, items): with open(os.path.join(destination, b"reader.txt"), "w") as f: f.write(narrator) - def on_import_task_created(self, session, task): + def on_import_task_created(self, session, task) -> None: """ Remember the items for the current task so manual album ID lookups can align tracks. This is needed because album_for_id doesn't give us the items being imported """ self._recent_items = list(task.items) - def before_choose_candidate_event(self, session, task): + def before_choose_candidate_event(self, session, task) -> Sequence[ui.commands.PromptChoice]: return [PromptChoice("r", "Region switch", self.book_level_region_switch)] - def book_level_region_switch(self, session, task): + def book_level_region_switch(self, session, task) -> None: """Prompts the book level region value""" available_region_codes = ", ".join(ui.colorize("text_diff_added", reg) for reg in AUDIBLE_REGIONS) @@ -639,7 +640,7 @@ def book_level_region_switch(self, session, task): task.lookup_candidates() -def get_item_region(item): +def get_item_region(item) -> str | None: """Get the value of the 'region' field, if it is available, or can be extracted from 'album_url'.""" available_field_names = item.keys() album_url = None diff --git a/beetsplug/book.py b/beetsplug/book.py index 447a1d8..1e0a972 100644 --- a/beetsplug/book.py +++ b/beetsplug/book.py @@ -118,7 +118,7 @@ def __init__( self.region = region @staticmethod - def from_audnex_book(b: dict): + def from_audnex_book(b: dict) -> Book: """ Creates a `Book` from an Audnex book result """ @@ -210,7 +210,7 @@ def __init__( self.runtime_length_sec = runtime_length_sec @staticmethod - def from_audnex_chapter_info(c: dict): + def from_audnex_chapter_info(c: dict) -> BookChapters: """ Creates a `BookChapters` instance from audnex's /book/{asin}/chapters endpoint """ diff --git a/beetsplug/goodreads.py b/beetsplug/goodreads.py index 56134d5..04b2cb0 100644 --- a/beetsplug/goodreads.py +++ b/beetsplug/goodreads.py @@ -50,7 +50,7 @@ def goodreads_get_total_result(response: Element) -> int: return int(response.findtext("./search/total-results")) -def parse_original_date(work: Element) -> dict: +def parse_original_date(work: Element) -> dict[str, int | None]: original_date = {} if work is not None: year = work.findtext("original_publication_year") From a5c242e0923ffa236775413d01166af26027c7ef Mon Sep 17 00:00:00 2001 From: Serene-Arc <33189705+Serene-Arc@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:13:57 +1000 Subject: [PATCH 3/9] Update optional type to new syntax --- beetsplug/api.py | 4 ++-- beetsplug/book.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/beetsplug/api.py b/beetsplug/api.py index 65ad238..a9e129b 100644 --- a/beetsplug/api.py +++ b/beetsplug/api.py @@ -1,7 +1,7 @@ import json import xml.etree.ElementTree as ET from time import sleep -from typing import Optional, AnyStr +from typing import AnyStr from urllib import parse, request from urllib.error import HTTPError @@ -63,7 +63,7 @@ def get_audible_album_url(asin: str, region: str) -> str: return f"https://www.audible.{AUDIBLE_REGIONS_SUFFIXES[region]}/pd/{asin}" -def get_audible_album_region(url: str) -> Optional[str]: +def get_audible_album_region(url: str) -> str | None: suffix = tldextract.extract(url).suffix if suffix in AUDIBLE_SUFFIXES_REGIONS: return AUDIBLE_SUFFIXES_REGIONS[suffix] diff --git a/beetsplug/book.py b/beetsplug/book.py index 1e0a972..7b12416 100644 --- a/beetsplug/book.py +++ b/beetsplug/book.py @@ -1,5 +1,4 @@ import re -from typing import Optional from markdownify import markdownify as md @@ -8,7 +7,7 @@ class Author: - asin: Optional[str] + asin: str | None name: str def __init__(self, asin, name): @@ -49,7 +48,7 @@ class Series: asin: str name: str # Yes, sadly its possible for series to not have a position - position: Optional[str] # e.g, "2", "8.5", "1-5" + position: str | None # e.g, "2", "8.5", "1-5" def __init__(self, asin, name, position): self.asin = asin @@ -69,13 +68,13 @@ class Book: publisher: str release_date: str # yyyy-mm-dd format runtime_length_min: int - series: Optional[Series] - subtitle: Optional[str] + series: Series | None + subtitle: str | None summary_html: str summary_markdown: str tags: list[Tag] # may be an empty list title: str - region: Optional[str] + region: str | None def __init__( self, From 437845e9fe6e0c4674d38ed6d4130248069e5e5f Mon Sep 17 00:00:00 2001 From: Serene-Arc <33189705+Serene-Arc@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:47:54 +1000 Subject: [PATCH 4/9] Format with ruff --- beetsplug/audible.py | 1 - 1 file changed, 1 deletion(-) diff --git a/beetsplug/audible.py b/beetsplug/audible.py index 2857807..ef6dc2a 100644 --- a/beetsplug/audible.py +++ b/beetsplug/audible.py @@ -132,7 +132,6 @@ def __init__(self): region = mediafile.MediaField() self.add_media_field("region", region) - def candidates(self, items, artist, album, va_likely) -> list[AlbumInfo]: """Returns a list of AlbumInfo objects for Audible search results matching an album and artist (if not various). From b9583a4d52c0089f98684b1c3b41cce0d14f9f31 Mon Sep 17 00:00:00 2001 From: Serene-Arc <33189705+Serene-Arc@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:50:26 +1000 Subject: [PATCH 5/9] Use different import --- beetsplug/audible.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/audible.py b/beetsplug/audible.py index ef6dc2a..a2d39b1 100644 --- a/beetsplug/audible.py +++ b/beetsplug/audible.py @@ -5,7 +5,7 @@ import urllib.error from contextlib import suppress from tempfile import NamedTemporaryFile -from typing import Sequence +from collections.abc import Sequence import mediafile import yaml From 0bf54183fa0f477789e9af4fe34c65a041df6643 Mon Sep 17 00:00:00 2001 From: Serene-Arc <33189705+Serene-Arc@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:51:10 +1000 Subject: [PATCH 6/9] Fix errors for self-referential objects --- beetsplug/book.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/book.py b/beetsplug/book.py index 7b12416..9e538c7 100644 --- a/beetsplug/book.py +++ b/beetsplug/book.py @@ -117,7 +117,7 @@ def __init__( self.region = region @staticmethod - def from_audnex_book(b: dict) -> Book: + def from_audnex_book(b: dict) -> "Book": """ Creates a `Book` from an Audnex book result """ @@ -209,7 +209,7 @@ def __init__( self.runtime_length_sec = runtime_length_sec @staticmethod - def from_audnex_chapter_info(c: dict) -> BookChapters: + def from_audnex_chapter_info(c: dict) -> "BookChapters": """ Creates a `BookChapters` instance from audnex's /book/{asin}/chapters endpoint """ From eab2a712a559de23a28e28e3c90c709c09463795 Mon Sep 17 00:00:00 2001 From: Serene-Arc <33189705+Serene-Arc@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:51:28 +1000 Subject: [PATCH 7/9] Sort imports --- beetsplug/audible.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/audible.py b/beetsplug/audible.py index a2d39b1..ff20074 100644 --- a/beetsplug/audible.py +++ b/beetsplug/audible.py @@ -3,9 +3,9 @@ import pathlib import re import urllib.error +from collections.abc import Sequence from contextlib import suppress from tempfile import NamedTemporaryFile -from collections.abc import Sequence import mediafile import yaml From 467bfe39af54d905a472e0b673e662567979bba8 Mon Sep 17 00:00:00 2001 From: Serene-Arc <33189705+Serene-Arc@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:11:46 +1000 Subject: [PATCH 8/9] Switch typing to list from Sequence --- beetsplug/audible.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beetsplug/audible.py b/beetsplug/audible.py index ff20074..435a74c 100644 --- a/beetsplug/audible.py +++ b/beetsplug/audible.py @@ -3,7 +3,6 @@ import pathlib import re import urllib.error -from collections.abc import Sequence from contextlib import suppress from tempfile import NamedTemporaryFile @@ -599,7 +598,7 @@ def on_album_matched(self, match) -> None: match.extra_tracks = extra_tracks match.distance = distance(all_items, match.info, item_info_pairs) - def before_choose_candidate_event(self, session, task) -> Sequence[ui.commands.PromptChoice]: + def before_choose_candidate_event(self, session, task) -> list[ui.commands.PromptChoice]: return [PromptChoice("r", "Region switch", self.book_level_region_switch)] def book_level_region_switch(self, session, task) -> None: From 9f2fe00e91a4c438bfc8372988eb0afcf553df01 Mon Sep 17 00:00:00 2001 From: Serene-Arc <33189705+Serene-Arc@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:58:04 +1000 Subject: [PATCH 9/9] Change AnyStr to bytes --- beetsplug/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beetsplug/api.py b/beetsplug/api.py index 9c150f8..7e1ce70 100644 --- a/beetsplug/api.py +++ b/beetsplug/api.py @@ -1,7 +1,6 @@ import json import xml.etree.ElementTree as ET from time import sleep -from typing import AnyStr from urllib import parse, request from urllib.error import HTTPError @@ -71,7 +70,7 @@ def get_audible_album_region(url: str) -> str | None: return None -def make_request(url: str) -> AnyStr | None: +def make_request(url: str) -> bytes | None: """Makes a request to the specified url and returns received response The request will be retried up to 3 times in case of failure. """