mirror of
https://github.com/beetbox/beets.git
synced 2026-09-16 08:29:30 -05:00
LRCLib stores track metadata independently of the lyrics themselves, so an
entry can come back with both `plainLyrics` and `syncedLyrics` null while
`instrumental` is False. `LRCLyrics.is_valid` accepted such an entry as a
match on duration alone, and `get_text` then returned `self.plain`, i.e.
None, despite being annotated `-> str`.
The None propagated into `Lyrics`, and the first access of its text raised
AttributeError: 'NoneType' object has no attribute 'splitlines'
`beet lyrics` surfaced this per track, but during an import the exception
escaped the pipeline stage and aborted the entire run: one such track
stranded every file queued behind it.
Treat an entry with no lyrics text as not a match, so the search moves on to
other candidates and backends and ultimately reports that no lyrics were
found. That is deliberately distinct from an instrumental track, where "no
lyrics" is itself the answer and the existing `instrumental` handling still
applies. Marking these as instrumental would assert something the API
response does not tell us.
Also fall back to synced lyrics when only `plainLyrics` is null, rather than
discarding lyrics we do have, and correct the annotations: `plainLyrics` and
`LRCLyrics.plain` are both nullable.
146 lines
3.8 KiB
Python
146 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from typing_extensions import NotRequired, TypedDict
|
|
|
|
JSONDict = dict[str, Any]
|
|
|
|
|
|
class LRCLibAPI:
|
|
class Item(TypedDict):
|
|
"""Lyrics data item returned by the LRCLib API."""
|
|
|
|
id: int
|
|
name: str
|
|
trackName: str
|
|
artistName: str
|
|
albumName: str
|
|
duration: float | None
|
|
instrumental: bool
|
|
# Both lyrics fields may be null, even when 'instrumental' is False.
|
|
plainLyrics: str | None
|
|
syncedLyrics: str | None
|
|
|
|
|
|
class GeniusAPI:
|
|
"""Genius API data types.
|
|
|
|
This documents *only* the fields that are used in the plugin.
|
|
:attr:`SearchResult` is an exception, since I thought some of the other
|
|
fields might be useful in the future.
|
|
"""
|
|
|
|
class DateComponents(TypedDict):
|
|
year: int
|
|
month: int
|
|
day: int
|
|
|
|
class Artist(TypedDict):
|
|
api_path: str
|
|
header_image_url: str
|
|
id: int
|
|
image_url: str
|
|
is_meme_verified: bool
|
|
is_verified: bool
|
|
name: str
|
|
url: str
|
|
|
|
class Stats(TypedDict):
|
|
unreviewed_annotations: int
|
|
hot: bool
|
|
|
|
class SearchResult(TypedDict):
|
|
annotation_count: int
|
|
api_path: str
|
|
artist_names: str
|
|
full_title: str
|
|
header_image_thumbnail_url: str
|
|
header_image_url: str
|
|
id: int
|
|
lyrics_owner_id: int
|
|
lyrics_state: str
|
|
path: str
|
|
primary_artist_names: str
|
|
pyongs_count: int | None
|
|
relationships_index_url: str
|
|
release_date_components: GeniusAPI.DateComponents
|
|
release_date_for_display: str
|
|
release_date_with_abbreviated_month_for_display: str
|
|
song_art_image_thumbnail_url: str
|
|
song_art_image_url: str
|
|
stats: GeniusAPI.Stats
|
|
title: str
|
|
title_with_featured: str
|
|
url: str
|
|
featured_artists: list[GeniusAPI.Artist]
|
|
primary_artist: GeniusAPI.Artist
|
|
primary_artists: list[GeniusAPI.Artist]
|
|
|
|
class SearchHit(TypedDict):
|
|
result: GeniusAPI.SearchResult
|
|
|
|
class SearchResponse(TypedDict):
|
|
hits: list[GeniusAPI.SearchHit]
|
|
|
|
class Search(TypedDict):
|
|
response: GeniusAPI.SearchResponse
|
|
|
|
class StatusResponse(TypedDict):
|
|
status: int
|
|
message: str
|
|
|
|
class Meta(TypedDict):
|
|
meta: GeniusAPI.StatusResponse
|
|
|
|
Response = Search | Meta
|
|
|
|
|
|
class GoogleCustomSearchAPI:
|
|
class Response(TypedDict):
|
|
"""Search response from the Google Custom Search API.
|
|
|
|
If the search returns no results, the :attr:`items` field is not found.
|
|
"""
|
|
|
|
items: NotRequired[list[GoogleCustomSearchAPI.Item]]
|
|
|
|
class Item(TypedDict):
|
|
"""A Google Custom Search API result item.
|
|
|
|
:attr:`title` field is shown to the user in the search interface, thus
|
|
it gets truncated with an ellipsis for longer queries. For most
|
|
results, the full title is available as ``og:title`` metatag found
|
|
under the :attr:`pagemap` field. Note neither this metatag nor the
|
|
``pagemap`` field is guaranteed to be present in the data.
|
|
"""
|
|
|
|
title: str
|
|
link: str
|
|
pagemap: NotRequired[GoogleCustomSearchAPI.Pagemap]
|
|
|
|
class Pagemap(TypedDict):
|
|
"""Pagemap data with a single meta tags dict in a list."""
|
|
|
|
metatags: list[JSONDict]
|
|
|
|
|
|
class TranslatorAPI:
|
|
class Language(TypedDict):
|
|
"""Language data returned by the translator API."""
|
|
|
|
language: str
|
|
score: float
|
|
|
|
class Translation(TypedDict):
|
|
"""Translation data returned by the translator API."""
|
|
|
|
text: str
|
|
to: str
|
|
|
|
class Response(TypedDict):
|
|
"""Response from the translator API."""
|
|
|
|
detectedLanguage: TranslatorAPI.Language
|
|
translations: list[TranslatorAPI.Translation]
|