diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b3472e412..96b230c59 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,6 +54,15 @@ jobs: run: | tox -vv -e py-cov + - name: Test latest Python version with tox and mypy + if: matrix.python-version == '3.10' + # continue-on-error is not ideal since it doesn't give a visible + # warning, but there doesn't seem to be anything better: + # https://github.com/actions/toolkit/issues/399 + continue-on-error: true + run: | + tox -vv -e py-mypy + - name: Test nightly Python version with tox if: matrix.python-version == '3.11-dev' # continue-on-error is not ideal since it doesn't give a visible diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 000000000..b47e5dff3 --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,5 @@ +[mypy] +# FIXME: Would be better to actually type the libraries (if under our control), +# or write our own stubs. For now, silence errors +ignore_missing_imports = True + diff --git a/beets/plugins.py b/beets/plugins.py index ed1f82d8f..c14d7b423 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -655,7 +655,7 @@ class MetadataSourcePlugin(metaclass=abc.ABCMeta): raise NotImplementedError @staticmethod - def get_artist(artists, id_key='id', name_key='name'): + def get_artist(artists, id_key='id', name_key='name', join_key=None): """Returns an artist string (all artists) and an artist_id (the main artist) for a list of artist object dicts. @@ -663,6 +663,8 @@ class MetadataSourcePlugin(metaclass=abc.ABCMeta): and 'the') to the front and strips trailing disambiguation numbers. It returns a tuple containing the comma-separated string of all normalized artists and the ``id`` of the main/first artist. + Alternatively a keyword can be used to combine artists together into a + single string by passing the join_key argument. :param artists: Iterable of artist dicts or lists returned by API. :type artists: list[dict] or list[list] @@ -673,12 +675,19 @@ class MetadataSourcePlugin(metaclass=abc.ABCMeta): to concatenate for the artist string (containing all artists). Defaults to 'name'. :type name_key: str or int + :param join_key: Key or index corresponding to a field containing a + keyword to use for combining artists into a single string, for + example "Feat.", "Vs.", "And" or similar. The default is None + which keeps the default behaviour (comma-separated). + :type join_key: str or int :return: Normalized artist string. :rtype: str """ artist_id = None - artist_names = [] - for artist in artists: + artist_string = "" + artists = list(artists) # In case a generator was passed. + total = len(artists) + for idx, artist in enumerate(artists): if not artist_id: artist_id = artist[id_key] name = artist[name_key] @@ -686,9 +695,15 @@ class MetadataSourcePlugin(metaclass=abc.ABCMeta): name = re.sub(r' \(\d+\)$', '', name) # Move articles to the front. name = re.sub(r'^(.*?), (a|an|the)$', r'\2 \1', name, flags=re.I) - artist_names.append(name) - artist = ', '.join(artist_names).replace(' ,', ',') or None - return artist, artist_id + # Use a join keyword if requested and available. + if idx < (total - 1): # Skip joining on last. + if join_key and artist.get(join_key, None): + name += f" {artist[join_key]} " + else: + name += ', ' + artist_string += name + + return artist_string, artist_id def _get_id(self, url_type, id_): """Parse an ID from its URL if necessary. diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 820a0acbd..103aa1107 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -180,6 +180,44 @@ class DiscogsPlugin(BeetsPlugin): self._log.debug('Connection error in album search', exc_info=True) return [] + def item_candidates(self, item, artist, title): + """Returns a list of TrackInfo objects for Search API results + matching ``title`` and ``artist``. + :param item: Singleton item to be matched. + :type item: beets.library.Item + :param artist: The artist of the track to be matched. + :type artist: str + :param title: The title of the track to be matched. + :type title: str + :return: Candidate TrackInfo objects. + :rtype: list[beets.autotag.hooks.TrackInfo] + """ + if not self.discogs_client: + return + + if not artist and not title: + self._log.debug('Skipping Discogs query. File missing artist and ' + 'title tags.') + + query = f'{artist} {title}' + try: + albums = self.get_albums(query) + except DiscogsAPIError as e: + self._log.debug('API Error: {0} (query: {1})', e, query) + if e.status_code == 401: + self.reset_auth() + return self.item_candidates(item, artist, title) + else: + return [] + except CONNECTION_ERRORS: + self._log.debug('Connection error in track search', exc_info=True) + candidates = [] + for album_cur in albums: + self._log.debug(u'searching within album {0}', album_cur.album) + candidates += album_cur.tracks + # first 10 results, don't overwhelm with options + return candidates[:10] + @staticmethod def extract_release_id_regex(album_id): """Returns the Discogs_id or None.""" @@ -302,7 +340,8 @@ class DiscogsPlugin(BeetsPlugin): return None artist, artist_id = MetadataSourcePlugin.get_artist( - [a.data for a in result.artists] + [a.data for a in result.artists], + join_key='join' ) album = re.sub(r' +', ' ', result.title) album_id = result.data['id'] @@ -350,9 +389,15 @@ class DiscogsPlugin(BeetsPlugin): for track in tracks: track.media = media track.medium_total = mediums.count(track.medium) + if not track.artist: # get_track_info often fails to find artist + track.artist = artist + if not track.artist_id: + track.artist_id = artist_id # Discogs does not have track IDs. Invent our own IDs as proposed # in #2336. track.track_id = str(album_id) + "-" + track.track_alt + track.data_url = data_url + track.data_source = 'Discogs' # Retrieve master release id (returns None if there isn't one). master_id = result.data.get('master_id') @@ -566,7 +611,8 @@ class DiscogsPlugin(BeetsPlugin): track_id = None medium, medium_index, _ = self.get_track_index(track['position']) artist, artist_id = MetadataSourcePlugin.get_artist( - track.get('artists', []) + track.get('artists', []), + join_key='join' ) length = self.get_track_length(track['duration']) return TrackInfo(title=title, track_id=track_id, artist=artist, diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index 55684a274..6d7e3d009 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -21,7 +21,6 @@ from beets.util import displayable_path import os import re - # Filename field extraction patterns. PATTERNS = [ # Useful patterns. @@ -29,11 +28,11 @@ PATTERNS = [ r'^(?P\d+)[\s.\-_]+(?P.+)[\-_](?P.+)[\-_](?P<tag>.*)$', r'^(?P<artist>.+)[\-_](?P<title>.+)$', r'^(?P<track>\d+)[\s.\-_]+(?P<artist>.+)[\-_](?P<title>.+)$', - r'^(?P<title>.+)$', r'^(?P<track>\d+)[\s.\-_]+(?P<title>.+)$', r'^(?P<track>\d+)\s+(?P<title>.+)$', r'^(?P<title>.+) by (?P<artist>.+)$', r'^(?P<track>\d+).*$', + r'^(?P<title>.+)$', ] # Titles considered "empty" and in need of replacement. @@ -85,7 +84,7 @@ def bad_title(title): return False -def apply_matches(d): +def apply_matches(d, log): """Given a mapping from items to field dicts, apply the fields to the objects. """ @@ -113,6 +112,7 @@ def apply_matches(d): for item in d: if not item.artist: item.artist = artist + log.info('Artist replaced with: {}'.format(item.artist)) # No artist field: remaining field is the title. else: @@ -122,8 +122,11 @@ def apply_matches(d): for item in d: if bad_title(item.title): item.title = str(d[item][title_field]) + log.info('Title replaced with: {}'.format(item.title)) + if 'track' in d[item] and item.track == 0: item.track = int(d[item]['track']) + log.info('Track replaced with: {}'.format(item.track)) # Plugin structure and hook into import process. @@ -131,32 +134,31 @@ def apply_matches(d): class FromFilenamePlugin(plugins.BeetsPlugin): def __init__(self): super().__init__() - self.register_listener('import_task_start', filename_task) + self.register_listener('import_task_start', self.filename_task) + def filename_task(self, task, session): + """Examine each item in the task to see if we can extract a title + from the filename. Try to match all filenames to a number of + regexps, starting with the most complex patterns and successively + trying less complex patterns. As soon as all filenames match the + same regex we can make an educated guess of which part of the + regex that contains the title. + """ + items = task.items if task.is_album else [task.item] -def filename_task(task, session): - """Examine each item in the task to see if we can extract a title - from the filename. Try to match all filenames to a number of - regexps, starting with the most complex patterns and successively - trying less complex patterns. As soon as all filenames match the - same regex we can make an educated guess of which part of the - regex that contains the title. - """ - items = task.items if task.is_album else [task.item] + # Look for suspicious (empty or meaningless) titles. + missing_titles = sum(bad_title(i.title) for i in items) - # Look for suspicious (empty or meaningless) titles. - missing_titles = sum(bad_title(i.title) for i in items) + if missing_titles: + # Get the base filenames (no path or extension). + names = {} + for item in items: + path = displayable_path(item.path) + name, _ = os.path.splitext(os.path.basename(path)) + names[item] = name - if missing_titles: - # Get the base filenames (no path or extension). - names = {} - for item in items: - path = displayable_path(item.path) - name, _ = os.path.splitext(os.path.basename(path)) - names[item] = name - - # Look for useful information in the filenames. - for pattern in PATTERNS: - d = all_matches(names, pattern) - if d: - apply_matches(d) + # Look for useful information in the filenames. + for pattern in PATTERNS: + d = all_matches(names, pattern) + if d: + apply_matches(d, self._log) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 15e02cf4f..758f092de 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -743,7 +743,9 @@ class LyricsPlugin(plugins.BeetsPlugin): 'fallback': None, 'force': False, 'local': False, - 'sources': self.SOURCES, + # Musixmatch is disabled by default as they are currently blocking + # requests with the beets user agent. + 'sources': [s for s in self.SOURCES if s != "musixmatch"], 'dist_thresh': 0.1, }) self.config['bing_client_secret'].redact = True diff --git a/docs/changelog.rst b/docs/changelog.rst old mode 100755 new mode 100644 index 2d9fc6247..93b71c191 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -36,7 +36,8 @@ New features: * Add :ref:`exact match <exact-match>` queries, using the prefixes ``=`` and ``=~``. :bug:`4251` -* :doc:`/plugins/discogs`: Permit appending style to genre +* :doc:`/plugins/discogs`: Permit appending style to genre. +* :doc:`plugins/discogs`: Implement item_candidates for matching singletons. * :doc:`/plugins/convert`: Add a new `auto_keep` option that automatically converts files but keeps the *originals* in the library. :bug:`1840` :bug:`4302` @@ -53,9 +54,17 @@ New features: what a new or changed smart playlist saved in the config is actually returning. :bug:`4573` +* :doc:`/plugins/fromfilename`: Add debug log messages that inform when the + plugin replaced bad (missing) artist, title or tracknumber metadata. + :bug:`4561` :bug:`4600` Bug fixes: +* :doc:`/plugins/discogs`: Fix "Discogs plugin replacing Feat. or Ft. with + a comma" by fixing an oversight that removed a functionality from the code + base when the MetadataSourcePlugin abstract class was introduced in PR's + #3335 and #3371. + :bug:`4401` * :doc:`/plugins/convert`: Set default ``max_bitrate`` value to ``None`` to avoid transcoding when this parameter is not set. :bug:`4472` * :doc:`/plugins/replaygain`: Avoid a crash when errors occur in the analysis @@ -126,6 +135,9 @@ Bug fixes: * :doc:`plugins/lyrics`: Fixed issue with Tekstowo backend not actually checking if the found song matches. :bug:`4406` +* :doc:`/plugins/fromfilename`: Fix failed detection of <track> <title> + filename patterns. + :bug:`4561` :bug:`4600` For packagers: @@ -133,11 +145,14 @@ For packagers: :bug:`4167` * The minimum required version of :pypi:`mediafile` is now 0.9.0. -Other new things: +Other changes: * :doc:`/plugins/limit`: Limit query results to head or tail (``lslimit`` command only) * :doc:`/plugins/fish`: Add ``--output`` option. +* :doc:`/plugins/lyrics`: Remove Musixmatch from default enabled sources as + they are currently blocking requests from the beets user agent. + :bug:`4585` 1.6.0 (November 27, 2021) ------------------------- diff --git a/docs/conf.py b/docs/conf.py index f8ed63f9d..cb7596c55 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,10 +18,10 @@ pygments_style = 'sphinx' # External links to the bug tracker and other sites. extlinks = { - 'bug': ('https://github.com/beetbox/beets/issues/%s', '#'), - 'user': ('https://github.com/%s', ''), - 'pypi': ('https://pypi.org/project/%s/', ''), - 'stdlib': ('https://docs.python.org/3/library/%s.html', ''), + 'bug': ('https://github.com/beetbox/beets/issues/%s', '#%s'), + 'user': ('https://github.com/%s', '%s'), + 'pypi': ('https://pypi.org/project/%s/', '%s'), + 'stdlib': ('https://docs.python.org/3/library/%s.html', '%s'), } linkcheck_ignore = [ diff --git a/docs/plugins/discogs.rst b/docs/plugins/discogs.rst index 53c6c2ac0..1203a9ca3 100644 --- a/docs/plugins/discogs.rst +++ b/docs/plugins/discogs.rst @@ -10,9 +10,11 @@ Installation ------------ To use the ``discogs`` plugin, first enable it in your configuration (see -:ref:`using-plugins`). Then, install the `python3-discogs-client`_ library by typing:: +:ref:`using-plugins`). Then, install the `python3-discogs-client`_ library by typing: - pip install python3-discogs-client +.. code-block:: console + + $ pip install python3-discogs-client You will also need to register for a `Discogs`_ account, and provide authentication credentials via a personal access token or an OAuth2 @@ -38,11 +40,19 @@ Authentication via Personal Access Token As an alternative to OAuth, you can get a token from Discogs and add it to your configuration. To get a personal access token (called a "user token" in the `python3-discogs-client`_ -documentation), login to `Discogs`_, and visit the -`Developer settings page -<https://www.discogs.com/settings/developers>`_. Press the ``Generate new -token`` button, and place the generated token in your configuration, as the -``user_token`` config option in the ``discogs`` section. +documentation): + +#. login to `Discogs`_; +#. visit the `Developer settings page <https://www.discogs.com/settings/developers>`_; +#. press the *Generate new token* button; +#. copy the generated token; +#. place it in your configuration in the ``discogs`` section as the ``user_token`` option: + + .. code-block:: yaml + + discogs: + user_token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + Configuration ------------- @@ -53,22 +63,30 @@ There is one additional option in the ``discogs:`` section, ``index_tracks``. Index tracks (see the `Discogs guidelines <https://support.discogs.com/hc/en-us/articles/360005055373-Database-Guidelines-12-Tracklisting#Index_Tracks_And_Headings>`_), along with headers, mark divisions between distinct works on the same release -or within works. When ``index_tracks`` is enabled:: +or within works. When ``index_tracks`` is enabled: + +.. code-block:: yaml discogs: index_tracks: yes beets will incorporate the names of the divisions containing each track into -the imported track's title. For example, importing +the imported track's title. + +For example, importing `this album <https://www.discogs.com/Handel-Sutherland-Kirkby-Kwella-Nelson-Watkinson-Bowman-Rolfe-Johnson-Elliott-Partridge-Thomas-The-A/release/2026070>`_ -would result in track names like:: +would result in track names like: + +.. code-block:: text Messiah, Part I: No.1: Sinfony Messiah, Part II: No.22: Chorus- Behold The Lamb Of God Athalia, Act I, Scene I: Sinfonia -whereas with ``index_tracks`` disabled you'd get:: +whereas with ``index_tracks`` disabled you'd get: + +.. code-block:: text No.1: Sinfony No.22: Chorus- Behold The Lamb Of God @@ -80,7 +98,7 @@ Other configurations available under ``discogs:`` are: - **append_style_genre**: Appends the Discogs style (if found) to the genre tag. This can be useful if you want more granular genres to categorize your music. For example, a release in Discogs might have a genre of "Electronic" and a style of "Techno": enabling this setting would set the genre to be "Electronic, Techno" (assuming default separator of ``", "``) instead of just "Electronic". - Default: ``false`` + Default: ``False`` - **separator**: How to join multiple genre and style values from Discogs into a string. Default: ``", "`` @@ -99,4 +117,7 @@ Here are two things you can try: * Make sure that your system clock is accurate. The Discogs servers can reject your request if your clock is too out of sync. +Matching tracks by Discogs ID is not yet supported. The ``--group-albums`` +option in album import mode provides an alternative to singleton mode for autotagging tracks that are not in album-related folders. + .. _python3-discogs-client: https://github.com/joalla/discogs_client diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 4de531099..8404ce716 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -132,13 +132,21 @@ following to your configuration:: Autotagger Extensions --------------------- -* :doc:`chroma`: Use acoustic fingerprinting to identify audio files with - missing or incorrect metadata. -* :doc:`discogs`: Search for releases in the `Discogs`_ database. -* :doc:`spotify`: Search for releases in the `Spotify`_ database. -* :doc:`deezer`: Search for releases in the `Deezer`_ database. -* :doc:`fromfilename`: Guess metadata for untagged tracks from their - filenames. +:doc:`chroma <chroma>` + Use acoustic fingerprinting to identify audio files with + missing or incorrect metadata. + +:doc:`discogs <discogs>` + Search for releases in the `Discogs`_ database. + +:doc:`spotify <spotify>` + Search for releases in the `Spotify`_ database. + +:doc:`deezer <deezer>` + Search for releases in the `Deezer`_ database. + +:doc:`fromfilename <fromfilename>` + Guess metadata for untagged tracks from their filenames. .. _Discogs: https://www.discogs.com/ .. _Spotify: https://www.spotify.com @@ -147,30 +155,69 @@ Autotagger Extensions Metadata -------- -* :doc:`absubmit`: Analyse audio with the `streaming_extractor_music`_ program and submit the metadata to the AcousticBrainz server -* :doc:`acousticbrainz`: Fetch various AcousticBrainz metadata -* :doc:`bpm`: Measure tempo using keystrokes. -* :doc:`bpsync`: Fetch updated metadata from Beatport. -* :doc:`edit`: Edit metadata from a text editor. -* :doc:`embedart`: Embed album art images into files' metadata. -* :doc:`fetchart`: Fetch album cover art from various sources. -* :doc:`ftintitle`: Move "featured" artists from the artist field to the title - field. -* :doc:`keyfinder`: Use the `KeyFinder`_ program to detect the musical - key from the audio. -* :doc:`importadded`: Use file modification times for guessing the value for - the `added` field in the database. -* :doc:`lastgenre`: Fetch genres based on Last.fm tags. -* :doc:`lastimport`: Collect play counts from Last.fm. -* :doc:`lyrics`: Automatically fetch song lyrics. -* :doc:`mbsync`: Fetch updated metadata from MusicBrainz. -* :doc:`metasync`: Fetch metadata from local or remote sources -* :doc:`mpdstats`: Connect to `MPD`_ and update the beets library with play - statistics (last_played, play_count, skip_count, rating). -* :doc:`parentwork`: Fetch work titles and works they are part of. -* :doc:`replaygain`: Calculate volume normalization for players that support it. -* :doc:`scrub`: Clean extraneous metadata from music files. -* :doc:`zero`: Nullify fields by pattern or unconditionally. +:doc:`absubmit <absubmit>` + Analyse audio with the `streaming_extractor_music`_ program and submit the metadata to the AcousticBrainz server + +:doc:`acousticbrainz <acousticbrainz>` + Fetch various AcousticBrainz metadata + +:doc:`bpm <bpm>` + Measure tempo using keystrokes. + +:doc:`bpsync <bpsync>` + Fetch updated metadata from Beatport. + +:doc:`edit <edit>` + Edit metadata from a text editor. + +:doc:`embedart <embedart>` + Embed album art images into files' metadata. + +:doc:`fetchart <fetchart>` + Fetch album cover art from various sources. + +:doc:`ftintitle <ftintitle>` + Move "featured" artists from the artist field to the title + field. + +:doc:`keyfinder <keyfinder>` + Use the `KeyFinder`_ program to detect the musical + key from the audio. + +:doc:`importadded <importadded>` + Use file modification times for guessing the value for + the `added` field in the database. + +:doc:`lastgenre <lastgenre>` + Fetch genres based on Last.fm tags. + +:doc:`lastimport <lastimport>` + Collect play counts from Last.fm. + +:doc:`lyrics <lyrics>` + Automatically fetch song lyrics. + +:doc:`mbsync <mbsync>` + Fetch updated metadata from MusicBrainz. + +:doc:`metasync <metasync>` + Fetch metadata from local or remote sources + +:doc:`mpdstats <mpdstats>` + Connect to `MPD`_ and update the beets library with play + statistics (last_played, play_count, skip_count, rating). + +:doc:`parentwork <parentwork>` + Fetch work titles and works they are part of. + +:doc:`replaygain <replaygain>` + Calculate volume normalization for players that support it. + +:doc:`scrub <scrub>` + Clean extraneous metadata from music files. + +:doc:`zero <zero>` + Nullify fields by pattern or unconditionally. .. _KeyFinder: http://www.ibrahimshaath.co.uk/keyfinder/ .. _streaming_extractor_music: https://acousticbrainz.org/download @@ -178,37 +225,75 @@ Metadata Path Formats ------------ -* :doc:`albumtypes`: Format album type in path formats. -* :doc:`bucket`: Group your files into bucket directories that cover different - field values ranges. -* :doc:`inline`: Use Python snippets to customize path format strings. -* :doc:`rewrite`: Substitute values in path formats. -* :doc:`the`: Move patterns in path formats (i.e., move "a" and "the" to the - end). +:doc:`albumtypes <albumtypes>` + Format album type in path formats. + +:doc:`bucket <bucket>` + Group your files into bucket directories that cover different + field values ranges. + +:doc:`inline <inline>` + Use Python snippets to customize path format strings. + +:doc:`rewrite <rewrite>` + Substitute values in path formats. + +:doc:`the <the>` + Move patterns in path formats (i.e., move "a" and "the" to the + end). Interoperability ---------------- -* :doc:`aura`: A server implementation of the `AURA`_ specification. -* :doc:`badfiles`: Check audio file integrity. -* :doc:`embyupdate`: Automatically notifies `Emby`_ whenever the beets library changes. -* :doc:`fish`: Adds `Fish shell`_ tab autocompletion to ``beet`` commands. -* :doc:`importfeeds`: Keep track of imported files via ``.m3u`` playlist file(s) or symlinks. -* :doc:`ipfs`: Import libraries from friends and get albums from them via ipfs. -* :doc:`kodiupdate`: Automatically notifies `Kodi`_ whenever the beets library - changes. -* :doc:`mpdupdate`: Automatically notifies `MPD`_ whenever the beets library - changes. -* :doc:`play`: Play beets queries in your music player. -* :doc:`playlist`: Use M3U playlists to query the beets library. -* :doc:`plexupdate`: Automatically notifies `Plex`_ whenever the beets library - changes. -* :doc:`smartplaylist`: Generate smart playlists based on beets queries. -* :doc:`sonosupdate`: Automatically notifies `Sonos`_ whenever the beets library - changes. -* :doc:`thumbnails`: Get thumbnails with the cover art on your album folders. -* :doc:`subsonicupdate`: Automatically notifies `Subsonic`_ whenever the beets - library changes. +:doc:`aura <aura>` + A server implementation of the `AURA`_ specification. + +:doc:`badfiles <badfiles>` + Check audio file integrity. + +:doc:`embyupdate <embyupdate>` + Automatically notifies `Emby`_ whenever the beets library changes. + +:doc:`fish <fish>` + Adds `Fish shell`_ tab autocompletion to ``beet`` commands. + +:doc:`importfeeds <importfeeds>` + Keep track of imported files via ``.m3u`` playlist file(s) or symlinks. + +:doc:`ipfs <ipfs>` + Import libraries from friends and get albums from them via ipfs. + +:doc:`kodiupdate <kodiupdate>` + Automatically notifies `Kodi`_ whenever the beets library + changes. + +:doc:`mpdupdate <mpdupdate>` + Automatically notifies `MPD`_ whenever the beets library + changes. + +:doc:`play <play>` + Play beets queries in your music player. + +:doc:`playlist <playlist>` + Use M3U playlists to query the beets library. + +:doc:`plexupdate <plexupdate>` + Automatically notifies `Plex`_ whenever the beets library + changes. + +:doc:`smartplaylist <smartplaylist>` + Generate smart playlists based on beets queries. + +:doc:`sonosupdate <sonosupdate>` + Automatically notifies `Sonos`_ whenever the beets library + changes. + +:doc:`thumbnails <thumbnails>` + Get thumbnails with the cover art on your album folders. + +:doc:`subsonicupdate <subsonicupdate>` + Automatically notifies `Subsonic`_ whenever the beets + library changes. .. _AURA: https://auraspec.readthedocs.io @@ -222,28 +307,65 @@ Interoperability Miscellaneous ------------- -* :doc:`bareasc`: Search albums and tracks with bare ASCII string matching. -* :doc:`bpd`: A music player for your beets library that emulates `MPD`_ and is - compatible with `MPD clients`_. -* :doc:`convert`: Transcode music and embed album art while exporting to - a different directory. -* :doc:`duplicates`: List duplicate tracks or albums. -* :doc:`export`: Export data from queries to a format. -* :doc:`filefilter`: Automatically skip files during the import process based - on regular expressions. -* :doc:`fuzzy`: Search albums and tracks with fuzzy string matching. -* :doc:`hook`: Run a command when an event is emitted by beets. -* :doc:`ihate`: Automatically skip albums and tracks during the import process. -* :doc:`info`: Print music files' tags to the console. -* :doc:`loadext`: Load SQLite extensions. -* :doc:`mbcollection`: Maintain your MusicBrainz collection list. -* :doc:`mbsubmit`: Print an album's tracks in a MusicBrainz-friendly format. -* :doc:`missing`: List missing tracks. -* `mstream`_: A music streaming server + webapp that can be used alongside beets. -* :doc:`random`: Randomly choose albums and tracks from your library. -* :doc:`spotify`: Create Spotify playlists from the Beets library. -* :doc:`types`: Declare types for flexible attributes. -* :doc:`web`: An experimental Web-based GUI for beets. +:doc:`bareasc <bareasc>` + Search albums and tracks with bare ASCII string matching. + +:doc:`bpd <bpd>` + A music player for your beets library that emulates `MPD`_ and is + compatible with `MPD clients`_. + +:doc:`convert <convert>` + Transcode music and embed album art while exporting to + a different directory. + +:doc:`duplicates <duplicates>` + List duplicate tracks or albums. + +:doc:`export <export>` + Export data from queries to a format. + +:doc:`filefilter <filefilter>` + Automatically skip files during the import process based + on regular expressions. + +:doc:`fuzzy <fuzzy>` + Search albums and tracks with fuzzy string matching. + +:doc:`hook <hook>` + Run a command when an event is emitted by beets. + +:doc:`ihate <ihate>` + Automatically skip albums and tracks during the import process. + +:doc:`info <info>` + Print music files' tags to the console. + +:doc:`loadext <loadext>` + Load SQLite extensions. + +:doc:`mbcollection <mbcollection>` + Maintain your MusicBrainz collection list. + +:doc:`mbsubmit <mbsubmit>` + Print an album's tracks in a MusicBrainz-friendly format. + +:doc:`missing <missing>` + List missing tracks. + +`mstream`_ + A music streaming server + webapp that can be used alongside beets. + +:doc:`random <random>` + Randomly choose albums and tracks from your library. + +:doc:`spotify <spotify>` + Create Spotify playlists from the Beets library. + +:doc:`types <types>` + Declare types for flexible attributes. + +:doc:`web <web>` + An experimental Web-based GUI for beets. .. _MPD: https://www.musicpd.org/ .. _MPD clients: https://mpd.wikia.com/wiki/Clients @@ -270,76 +392,106 @@ line in your config file. Here are a few of the plugins written by the beets community: -* `beets-alternatives`_ manages external files. +`beets-alternatives`_ + Manages external files. -* `beet-amazon`_ adds Amazon.com as a tagger data source. +`beet-amazon`_ + Adds Amazon.com as a tagger data source. -* `beets-artistcountry`_ fetches the artist's country of origin from - MusicBrainz. +`beets-artistcountry`_ + Fetches the artist's country of origin from MusicBrainz. -* `beets-autofix`_ automates repetitive tasks to keep your library in order. +`beets-autofix`_ + Automates repetitive tasks to keep your library in order. -* `beets-audible`_ adds Audible as a tagger data source and provides - other features for managing audiobook collections. +`beets-audible`_ + Adds Audible as a tagger data source and provides + other features for managing audiobook collections. -* `beets-barcode`_ lets you scan or enter barcodes for physical media to - search for their metadata. +`beets-barcode`_ + Lets you scan or enter barcodes for physical media to + search for their metadata. -* `beetcamp`_ enables **bandcamp.com** autotagger with a fairly extensive amount of metadata. +`beetcamp`_ + Enables **bandcamp.com** autotagger with a fairly extensive amount of metadata. -* `beetstream`_ is server implementation of the `SubSonic API`_ specification, allowing you to stream your music on a multitude of clients. +`beetstream`_ + Is server implementation of the `SubSonic API`_ specification, allowing you to stream your music on a multitude of clients. -* `beets-bpmanalyser`_ analyses songs and calculates their tempo (BPM). +`beets-bpmanalyser`_ + Analyses songs and calculates their tempo (BPM). -* `beets-check`_ automatically checksums your files to detect corruption. +`beets-check`_ + Automatically checksums your files to detect corruption. -* `A cmus plugin`_ integrates with the `cmus`_ console music player. +`A cmus plugin`_ + Integrates with the `cmus`_ console music player. -* `beets-copyartifacts`_ helps bring non-music files along during import. +`beets-copyartifacts`_ + Helps bring non-music files along during import. -* `beets-describe`_ gives you the full picture of a single attribute of your library items. +`beets-describe`_ + Gives you the full picture of a single attribute of your library items. -* `drop2beets`_ automatically imports singles as soon as they are dropped in a - folder (using Linux's ``inotify``). You can also set a sub-folders - hierarchy to set flexible attributes by the way. +`drop2beets`_ + Automatically imports singles as soon as they are dropped in a + folder (using Linux's ``inotify``). You can also set a sub-folders + hierarchy to set flexible attributes by the way. -* `dsedivec`_ has two plugins: ``edit`` and ``moveall``. +`dsedivec`_ + Has two plugins: ``edit`` and ``moveall``. -* `beets-follow`_ lets you check for new albums from artists you like. +`beets-follow`_ + Lets you check for new albums from artists you like. -* `beetFs`_ is a FUSE filesystem for browsing the music in your beets library. - (Might be out of date.) +`beetFs`_ + Is a FUSE filesystem for browsing the music in your beets library. + (Might be out of date.) -* `beets-goingrunning`_ generates playlists to go with your running sessions. +`beets-goingrunning`_ + Generates playlists to go with your running sessions. -* `beets-ibroadcast`_ uploads tracks to the `iBroadcast`_ cloud service. +`beets-ibroadcast`_ + Uploads tracks to the `iBroadcast`_ cloud service. -* `beets-importreplace`_ lets you perform regex replacements on incoming - metadata. +`beets-importreplace`_ + Lets you perform regex replacements on incoming + metadata. -* `beets-mosaic`_ generates a montage of a mosaic from cover art. +`beets-mosaic`_ + Generates a montage of a mosaic from cover art. -* `beets-noimport`_ adds and removes directories from the incremental import skip list. +`beets-noimport`_ + Adds and removes directories from the incremental import skip list. -* `beets-originquery`_ augments MusicBrainz queries with locally-sourced data - to improve autotagger results. +`beets-originquery`_ + Augments MusicBrainz queries with locally-sourced data + to improve autotagger results. -* `beets-popularity`_ fetches popularity values from Deezer. +`beets-popularity`_ + Fetches popularity values from Deezer. -* `beets-setlister`_ generate playlists from the setlists of a given artist. +`beets-setlister`_ + Generate playlists from the setlists of a given artist. -* `beet-summarize`_ can compute lots of counts and statistics about your music - library. +`beet-summarize`_ + Can compute lots of counts and statistics about your music + library. -* `beets-usertag`_ lets you use keywords to tag and organize your music. +`beets-usertag`_ + Lets you use keywords to tag and organize your music. -* `whatlastgenre`_ fetches genres from various music sites. +`whatlastgenre`_ + Fetches genres from various music sites. -* `beets-xtractor`_ extracts low- and high-level musical information from your songs. +`beets-xtractor`_ + Extracts low- and high-level musical information from your songs. -* `beets-ydl`_ downloads audio from youtube-dl sources and import into beets. +`beets-ydl`_ + Downloads audio from youtube-dl sources and import into beets. -* `beets-yearfixer`_ attempts to fix all missing ``original_year`` and ``year`` fields. +`beets-yearfixer`_ + Attempts to fix all missing ``original_year`` and ``year`` fields. .. _beets-barcode: https://github.com/8h2a/beets-barcode .. _beetcamp: https://github.com/snejus/beetcamp diff --git a/docs/plugins/lyrics.rst b/docs/plugins/lyrics.rst index 7c5a3773c..90c455e15 100644 --- a/docs/plugins/lyrics.rst +++ b/docs/plugins/lyrics.rst @@ -2,10 +2,9 @@ Lyrics Plugin ============= The ``lyrics`` plugin fetches and stores song lyrics from databases on the Web. -Namely, the current version of the plugin uses `Musixmatch`_, `Genius.com`_, -`Tekstowo.pl`_, and, optionally, the Google custom search API. +Namely, the current version of the plugin uses `Genius.com`_, `Tekstowo.pl`_, +and, optionally, the Google custom search API. -.. _Musixmatch: https://www.musixmatch.com/ .. _Genius.com: https://genius.com/ .. _Tekstowo.pl: https://www.tekstowo.pl/ @@ -59,9 +58,9 @@ configuration file. The available options are: sources known to be scrapeable. - **sources**: List of sources to search for lyrics. An asterisk ``*`` expands to all available sources. - Default: ``google musixmatch genius tekstowo``, i.e., all the - available sources. The ``google`` source will be automatically - deactivated if no ``google_API_key`` is setup. + Default: ``google genius tekstowo``, i.e., all the available sources. The + ``google`` source will be automatically deactivated if no ``google_API_key`` + is setup. The ``google``, ``genius``, and ``tekstowo`` sources will only be enabled if BeautifulSoup is installed. @@ -139,8 +138,7 @@ configuration option to your key. Then add ``google`` to the list of sources in your configuration (or use default list, which includes it as long as you have an API key). If you use default ``google_engine_ID``, we recommend limiting the sources to -``musixmatch google`` as the other sources are already included in the Google -results. +``google`` as the other sources are already included in the Google results. .. _register for a Google API key: https://console.developers.google.com/ diff --git a/docs/plugins/plexupdate.rst b/docs/plugins/plexupdate.rst index b6a2bf920..aaeb28e5b 100644 --- a/docs/plugins/plexupdate.rst +++ b/docs/plugins/plexupdate.rst @@ -8,18 +8,22 @@ To use ``plexupdate`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then, you'll probably want to configure the specifics of your Plex server. You can do that using an ``plex:`` section in your ``config.yaml``, -which looks like this:: +which looks like this: - plex: - host: localhost - port: 32400 - token: token +.. code-block:: yaml + + plex: + host: "localhost" + port: 32400 + token: "TOKEN" The ``token`` key is optional: you'll need to use it when in a Plex Home (see Plex's own `documentation about tokens`_). To use the ``plexupdate`` plugin you need to install the `requests`_ library with: - pip install requests +.. code-block:: console + + $ pip install beets[plexupdate] With that all in place, you'll see beets send the "update" command to your Plex server every time you change your beets library. @@ -44,4 +48,4 @@ The available options under the ``plex:`` section are: - **secure**: Use secure connections to the Plex server. Default: ``False`` - **ignore_cert_errors**: Ignore TLS certificate errors when using secure connections. - Default: ``False`` + Default: ``False`` \ No newline at end of file diff --git a/docs/reference/config.rst b/docs/reference/config.rst index e59937dc3..b6fa8fea6 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -135,7 +135,7 @@ unexpected behavior on all popular platforms:: These substitutions remove forward and back slashes, leading dots, and control characters—all of which is a good idea on any OS. The fourth line -removes the Windows "reserved characters" (useful even on Unix for for +removes the Windows "reserved characters" (useful even on Unix for compatibility with Windows-influenced network filesystems like Samba). Trailing dots and trailing whitespace, which can cause problems on Windows clients, are also removed. diff --git a/setup.py b/setup.py index d49ed65b2..a6984ffd2 100755 --- a/setup.py +++ b/setup.py @@ -119,6 +119,15 @@ setup( 'flake8-docstrings', 'pep8-naming', ], + 'mypy': [ + 'mypy', + 'types-Pillow', + 'types-urllib3', + 'types-beautifulsoup4', + 'types-PyYAML', + 'types-requests', + 'types-Flask-Cors', + ], # Plugin (optional) dependencies: 'absubmit': ['requests'], diff --git a/test/test_embedart.py b/test/test_embedart.py index f41180ec1..6cf5bfa56 100644 --- a/test/test_embedart.py +++ b/test/test_embedart.py @@ -82,7 +82,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self.run_command('embedart', '-f', self.small_artpath) mediafile = MediaFile(syspath(item.path)) # make sure that images array is empty (nothing embedded) - self.assertEqual(len(mediafile.images), 0) + self.assertFalse(mediafile.images) def test_embed_art_from_file(self): self._setup_data() @@ -203,7 +203,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self.io.addinput('y') self.run_command('clearart') mediafile = MediaFile(syspath(item.path)) - self.assertEqual(len(mediafile.images), 0) + self.assertFalse(mediafile.images) def test_clear_art_with_no_input(self): self._setup_data() diff --git a/test/test_ui.py b/test/test_ui.py index ad4387013..dd393035b 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -701,6 +701,28 @@ class UpdateTest(_common.TestCase): item = self.lib.items().get() self.assertEqual(item.title, 'full') + @unittest.expectedFailure + def test_multivalued_albumtype_roundtrip(self): + # https://github.com/beetbox/beets/issues/4528 + + # albumtypes is empty for our test fixtures, so populate it first + album = self.album + # setting albumtypes does not set albumtype currently... + # FIXME: When actually fixing the issue 4528, consider whether this + # should be set to "album" or ["album"] + album.albumtype = "album" + album.albumtypes = "album" + album.try_sync(write=True, move=False) + + album.load() + albumtype_before = album.albumtype + self.assertEqual(albumtype_before, "album") + + self._update() + + album.load() + self.assertEqual(albumtype_before, album.albumtype) + class PrintTest(_common.TestCase): def setUp(self): diff --git a/test/test_zero.py b/test/test_zero.py index c4c176960..b48367b41 100644 --- a/test/test_zero.py +++ b/test/test_zero.py @@ -103,7 +103,7 @@ class ZeroPluginTest(unittest.TestCase, TestHelper): item.write() mf = MediaFile(syspath(path)) - self.assertEqual(0, len(mf.images)) + self.assertFalse(mf.images) def test_auto_false(self): self.config['zero']['fields'] = ['year'] diff --git a/tox.ini b/tox.ini index 1c0a984ed..861101e0f 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py38-{cov,lint}, docs +envlist = py38-{cov,lint,mypy}, docs [_test] deps = .[test] @@ -13,15 +13,23 @@ deps = .[test] deps = .[lint] files = beets beetsplug beet test setup.py docs +[_mypy] +deps = + .[mypy] + .[test] + [testenv] deps = {test,cov}: {[_test]deps} lint: {[_lint]deps} + mypy: {[_mypy]deps} passenv = INTEGRATION_TEST commands = test: python -bb -m pytest -rs {posargs} cov: coverage run -m pytest -rs {posargs} lint: python -m flake8 {posargs} {[_lint]files} + mypy: mypy -p beets -p beetsplug + mypy: mypy test [testenv:docs] basepython = python3.10