From ca8c557840389f85c2f8a4803d12853caf1a658c Mon Sep 17 00:00:00 2001 From: Nathan Musoke Date: Tue, 9 May 2017 14:19:01 +1200 Subject: [PATCH 001/293] bugfix: Python3ify the IPFS plugin Paths were being constructed in a Python 3-incompatible way by concating bytes and strings. Do this more carefully by encoding and decoding of binary and strings. --- beetsplug/ipfs.py | 12 ++++++------ docs/changelog.rst | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/beetsplug/ipfs.py b/beetsplug/ipfs.py index 9a9d6aa50..88e305213 100644 --- a/beetsplug/ipfs.py +++ b/beetsplug/ipfs.py @@ -190,7 +190,7 @@ class IPFSPlugin(BeetsPlugin): else: lib_name = _hash lib_root = os.path.dirname(lib.path) - remote_libs = lib_root + "/remotes" + remote_libs = os.path.join(lib_root, b"remotes") if not os.path.exists(remote_libs): try: os.makedirs(remote_libs) @@ -198,7 +198,7 @@ class IPFSPlugin(BeetsPlugin): msg = "Could not create {0}. Error: {1}".format(remote_libs, e) self._log.error(msg) return False - path = remote_libs + "/" + lib_name + ".db" + path = os.path.join(remote_libs, lib_name.encode() + b".db") if not os.path.exists(path): cmd = "ipfs get {0} -o".format(_hash).split() cmd.append(path) @@ -209,7 +209,7 @@ class IPFSPlugin(BeetsPlugin): return False # add all albums from remotes into a combined library - jpath = remote_libs + "/joined.db" + jpath = os.path.join(remote_libs, b"joined.db") jlib = library.Library(jpath) nlib = library.Library(path) for album in nlib.albums(): @@ -237,7 +237,7 @@ class IPFSPlugin(BeetsPlugin): return for album in albums: - ui.print_(format(album, fmt), " : ", album.ipfs) + ui.print_(format(album, fmt), " : ", album.ipfs.decode()) def query(self, lib, args): rlib = self.get_remote_lib(lib) @@ -246,8 +246,8 @@ class IPFSPlugin(BeetsPlugin): def get_remote_lib(self, lib): lib_root = os.path.dirname(lib.path) - remote_libs = lib_root + "/remotes" - path = remote_libs + "/joined.db" + remote_libs = os.path.join(lib_root, b"remotes") + path = os.path.join(remote_libs, b"joined.db") if not os.path.isfile(path): raise IOError return library.Library(path) diff --git a/docs/changelog.rst b/docs/changelog.rst index bf0d7d254..b80df36c9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -102,6 +102,7 @@ Fixes: error message. Thanks to :user:`Mary011196`. :bug:`1676` :bug:`2508` * :doc:`/plugins/web`: Avoid a crash when sending binary data, such as Chromaprint fingerprints, in music attributes. :bug:`2542` :bug:`2532` +* :doc:`/plugins/ipfs`: Fix Python 3 compatibility. Two plugins had backends removed due to bitrot: From c5319274ca4e8322c250d0b7a9ae9ac036ec4432 Mon Sep 17 00:00:00 2001 From: Nathan Musoke Date: Tue, 9 May 2017 15:38:50 +1200 Subject: [PATCH 002/293] IPFS plugin: Add note to check hashes carefully In the future, just checking that a hash begins with "Qm" and has length 46 will likely not be sufficient. --- beetsplug/ipfs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beetsplug/ipfs.py b/beetsplug/ipfs.py index 88e305213..0e9b0c1f1 100644 --- a/beetsplug/ipfs.py +++ b/beetsplug/ipfs.py @@ -147,6 +147,8 @@ class IPFSPlugin(BeetsPlugin): def ipfs_get(self, lib, query): query = query[0] # Check if query is a hash + # TODO: generalize to other hashes; probably use a multihash + # implementation if query.startswith("Qm") and len(query) == 46: self.ipfs_get_from_hash(lib, query) else: From 25e55a88a3acf2c6324aac28395b7d5782fa5219 Mon Sep 17 00:00:00 2001 From: RollingStar Date: Wed, 30 Oct 2019 20:23:18 -0400 Subject: [PATCH 003/293] Clarify getitem docstring. Log when falling back. I figure, let someone change the docstring again if the function gets a special case besides artist / album artist. --- beets/library.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beets/library.py b/beets/library.py index 5d90ae43b..d5fb5fa75 100644 --- a/beets/library.py +++ b/beets/library.py @@ -410,7 +410,8 @@ class FormattedItemMapping(dbcore.db.FormattedMapping): raise KeyError(key) def __getitem__(self, key): - """Get the value for a key. Certain unset values are remapped. + """Get the value for a key. `artist` and `albumartist` + are fallback values for each other when unmapped. """ value = self._get(key) @@ -418,8 +419,10 @@ class FormattedItemMapping(dbcore.db.FormattedMapping): # This is helpful in path formats when the album artist is unset # on as-is imports. if key == 'artist' and not value: + log.debug('No artist, using album artist {0}'.format(value)) return self._get('albumartist') elif key == 'albumartist' and not value: + log.debug('No albumartist, using artist {0}'.format(value)) return self._get('artist') else: return value From 59dec3f4da17d107fc15a7108e02592cf09df8ba Mon Sep 17 00:00:00 2001 From: RollingStar Date: Wed, 30 Oct 2019 20:29:32 -0400 Subject: [PATCH 004/293] Log the new value --- beets/library.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/beets/library.py b/beets/library.py index d5fb5fa75..257197149 100644 --- a/beets/library.py +++ b/beets/library.py @@ -419,11 +419,13 @@ class FormattedItemMapping(dbcore.db.FormattedMapping): # This is helpful in path formats when the album artist is unset # on as-is imports. if key == 'artist' and not value: - log.debug('No artist, using album artist {0}'.format(value)) - return self._get('albumartist') + new_value = self._get('albumartist') + log.debug('No artist, using album artist {0}'.format(new_value)) + return new_value elif key == 'albumartist' and not value: - log.debug('No albumartist, using artist {0}'.format(value)) - return self._get('artist') + new_value = self._get('artist') + log.debug('No albumartist, using artist {0}'.format(new_value)) + return new_value else: return value From 918d833e35c6230e74465773ab75399811af8d5a Mon Sep 17 00:00:00 2001 From: RollingStar Date: Wed, 30 Oct 2019 20:30:25 -0400 Subject: [PATCH 005/293] fix(?) tabs --- beets/library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/library.py b/beets/library.py index 257197149..d9afac677 100644 --- a/beets/library.py +++ b/beets/library.py @@ -419,11 +419,11 @@ class FormattedItemMapping(dbcore.db.FormattedMapping): # This is helpful in path formats when the album artist is unset # on as-is imports. if key == 'artist' and not value: - new_value = self._get('albumartist') + new_value = self._get('albumartist') log.debug('No artist, using album artist {0}'.format(new_value)) return new_value elif key == 'albumartist' and not value: - new_value = self._get('artist') + new_value = self._get('artist') log.debug('No albumartist, using artist {0}'.format(new_value)) return new_value else: From 73ede2642db21b5a3f3e929cc2d6901f580605d6 Mon Sep 17 00:00:00 2001 From: RollingStar Date: Wed, 30 Oct 2019 20:33:21 -0400 Subject: [PATCH 006/293] Better docstring; fix tabs again --- beets/library.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beets/library.py b/beets/library.py index d9afac677..3ad875991 100644 --- a/beets/library.py +++ b/beets/library.py @@ -411,7 +411,7 @@ class FormattedItemMapping(dbcore.db.FormattedMapping): def __getitem__(self, key): """Get the value for a key. `artist` and `albumartist` - are fallback values for each other when unmapped. + are fallback values for each other when not set. """ value = self._get(key) @@ -419,11 +419,11 @@ class FormattedItemMapping(dbcore.db.FormattedMapping): # This is helpful in path formats when the album artist is unset # on as-is imports. if key == 'artist' and not value: - new_value = self._get('albumartist') + new_value = self._get('albumartist') log.debug('No artist, using album artist {0}'.format(new_value)) return new_value elif key == 'albumartist' and not value: - new_value = self._get('artist') + new_value = self._get('artist') log.debug('No albumartist, using artist {0}'.format(new_value)) return new_value else: From 0191794285422eb4173421c04065f704f14a2b34 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 10 Nov 2019 21:59:58 +0100 Subject: [PATCH 007/293] subsonicplaylist plugin --- beetsplug/subsonicplaylist.py | 126 ++++++++++++++++++++++++++++++ docs/changelog.rst | 1 + docs/plugins/index.rst | 1 + docs/plugins/subsonicplaylist.rst | 24 ++++++ 4 files changed, 152 insertions(+) create mode 100644 beetsplug/subsonicplaylist.py create mode 100644 docs/plugins/subsonicplaylist.rst diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py new file mode 100644 index 000000000..5b7ed95bc --- /dev/null +++ b/beetsplug/subsonicplaylist.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2019, Joris Jensen +# +# 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: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +from __future__ import absolute_import, division, print_function + +import os +from hashlib import md5 +import xml.etree.ElementTree as ET +import random +import string +import requests +from beets.util import normpath, bytestring_path, mkdirall, syspath, \ + path_as_posix, sanitize_path + +from beets.ui import Subcommand + +from beets.plugins import BeetsPlugin + +__author__ = 'https://github.com/MrNuggelz' + + +class SubsonicPlaylistPlugin(BeetsPlugin): + + def __init__(self): + super(SubsonicPlaylistPlugin, self).__init__() + self.config.add( + { + 'relative_to': None, + 'playlist_dir': '.', + 'forward_slash': False, + 'playlist_ids': [], + 'playlist_names': [], + 'username': '', + 'password': '' + } + ) + self.config['password'].redact = True + + def create_playlist(self, xml, lib): + relative_to = self.config['relative_to'].get() + if relative_to: + relative_to = normpath(relative_to) + + playlist = ET.fromstring(xml)[0] + if playlist.attrib.get('code', '200') != '200': + alt_error = 'error getting playlist, but no error message found' + self._log.warn(playlist.attrib.get('message', alt_error)) + return + name = '{}.m3u'.format() + tracks = [(t.attrib['artist'], t.attrib['album'], t.attrib['title']) + for t in playlist] + track_paths = [] + for t in tracks: + query = 'artist:"{}" album:"{}" title:"{}"'.format(*t) + items = lib.items(query) + if len(items) > 0: + item_path = items[0].path + if relative_to: + item_path = os.path.relpath(items[0].path, relative_to) + track_paths.append(item_path) + else: + self._log.warn(u"{} | track not found ({})", name, query) + # write playlist + playlist_dir = self.config['playlist_dir'].as_filename() + playlist_dir = bytestring_path(playlist_dir) + m3u_path = normpath(os.path.join(playlist_dir, bytestring_path(name))) + mkdirall(m3u_path) + with open(syspath(m3u_path), 'wb') as f: + for path in track_paths: + if self.config['forward_slash'].get(): + path = path_as_posix(path) + f.write(path + b'\n') + + def get_playlist_by_id(self, playlist_id, lib): + xml = self.send('getPlaylist', '&id={}'.format(playlist_id)).text + self.create_playlist(xml, lib) + + def commands(self): + def build_playlist(lib, opts, args): + + if len(self.config['playlist_ids'].as_str_seq()) > 0: + for playlist_id in self.config['playlist_ids'].as_str_seq(): + self.get_playlist_by_id(playlist_id, lib) + if len(self.config['playlist_names'].as_str_seq()) > 0: + playlists = ET.fromstring(self.send('getPlaylists').text)[0] + if playlists.attrib.get('code', '200') != '200': + alt_error = 'error getting playlists,' \ + ' but no erro message found' + self._log.warn(playlists.attrib.get('message', alt_error)) + return + for name in self.config['playlist_names'].as_str_seq(): + for playlist in playlists: + if name == playlist.attrib['name']: + self.get_playlist_by_id(playlist.attrib['id'], lib) + + subsonicplaylist_cmds = Subcommand( + 'subsonicplaylist', help=u'import a subsonic playlist' + ) + subsonicplaylist_cmds.func = build_playlist + return [subsonicplaylist_cmds] + + def generate_token(self): + salt = ''.join(random.choices(string.ascii_lowercase + string.digits)) + return md5( + (self.config['password'].get() + salt).encode()).hexdigest(), salt + + def send(self, endpoint, params=''): + url = '{}/rest/{}?u={}&t={}&s={}&v=1.12.0&c=beets'.format( + self.config['base_url'].get(), + endpoint, + self.config['username'], + *self.generate_token()) + resp = requests.get(url + params) + return resp diff --git a/docs/changelog.rst b/docs/changelog.rst index 169da71ba..9a8d1ae5b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog New features: +* :doc:`/plugins/subsonicplaylist`: import playlist from a subsonic server. * :doc:`plugins/discogs` now adds two extra fields: `discogs_labelid` and `discogs_artistid` :bug: `3413` diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 066d6ec22..9cf2cc5a9 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -115,6 +115,7 @@ following to your configuration:: smartplaylist sonosupdate spotify + subsonicplaylist subsonicupdate the thumbnails diff --git a/docs/plugins/subsonicplaylist.rst b/docs/plugins/subsonicplaylist.rst new file mode 100644 index 000000000..d6d64f60b --- /dev/null +++ b/docs/plugins/subsonicplaylist.rst @@ -0,0 +1,24 @@ +Subsonic Playlist Plugin +======================== + +The ``subsonicplaylist`` plugin allows to import playlist from a subsonic server + +Command Line Usage +------------------ + +To use the ``subsonicplaylist`` plugin, enable it in your configuration (see +:ref:`using-plugins`). Then use it by invoking the ``subsonicplaylist`` command. +The command will search the playlist on the subsonic server and create a playlist +using the beets library. Options to be defined in your config with their default value:: + + subsonicplaylist: + base_url: "https://your.subsonic.server" + 'relative_to': None, + 'playlist_dir': '.', + 'forward_slash': False, + 'playlist_ids': [], + 'playlist_names': [], + 'username': '', + 'password': '' + +Parameters `base_url`, `username` and `password` must be defined! \ No newline at end of file From ec696f5e58aa83240b578642fad9714455a3828e Mon Sep 17 00:00:00 2001 From: Joris Date: Mon, 11 Nov 2019 10:48:29 +0100 Subject: [PATCH 008/293] Removed unused sanitize_path import --- beetsplug/subsonicplaylist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 5b7ed95bc..9d3ee7cf6 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -22,7 +22,7 @@ import random import string import requests from beets.util import normpath, bytestring_path, mkdirall, syspath, \ - path_as_posix, sanitize_path + path_as_posix from beets.ui import Subcommand From bb305b17e14ee36bc46594418913d43c6128e02b Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sat, 16 Nov 2019 14:36:48 +0100 Subject: [PATCH 009/293] set tags instead of creating m3u --- beetsplug/subsonicplaylist.py | 74 +++++++++++++++-------------------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 9d3ee7cf6..1dd73f939 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -15,14 +15,11 @@ from __future__ import absolute_import, division, print_function -import os from hashlib import md5 import xml.etree.ElementTree as ET import random import string import requests -from beets.util import normpath, bytestring_path, mkdirall, syspath, \ - path_as_posix from beets.ui import Subcommand @@ -37,9 +34,6 @@ class SubsonicPlaylistPlugin(BeetsPlugin): super(SubsonicPlaylistPlugin, self).__init__() self.config.add( { - 'relative_to': None, - 'playlist_dir': '.', - 'forward_slash': False, 'playlist_ids': [], 'playlist_names': [], 'username': '', @@ -48,62 +42,56 @@ class SubsonicPlaylistPlugin(BeetsPlugin): ) self.config['password'].redact = True - def create_playlist(self, xml, lib): - relative_to = self.config['relative_to'].get() - if relative_to: - relative_to = normpath(relative_to) + def update_tags(self, playlist_dict, lib): + for query, playlist_tag in playlist_dict.items(): + query = 'artist:"{}" album:"{}" title:"{}"'.format(*query) + items = lib.items(query) + if len(items) <= 0: + self._log.warn(u"{} | track not found ({})", playlist_tag, + query) + continue + for item in items: + item.update({'subsonic_playlist': playlist_tag}) + with lib.transaction(): + item.try_sync(write=True, move=False) + def get_playlist(self, playlist_id): + xml = self.send('getPlaylist', '&id={}'.format(playlist_id)).text playlist = ET.fromstring(xml)[0] if playlist.attrib.get('code', '200') != '200': alt_error = 'error getting playlist, but no error message found' self._log.warn(playlist.attrib.get('message', alt_error)) return - name = '{}.m3u'.format() + + name = playlist.attrib.get('name', 'undefined') tracks = [(t.attrib['artist'], t.attrib['album'], t.attrib['title']) for t in playlist] - track_paths = [] - for t in tracks: - query = 'artist:"{}" album:"{}" title:"{}"'.format(*t) - items = lib.items(query) - if len(items) > 0: - item_path = items[0].path - if relative_to: - item_path = os.path.relpath(items[0].path, relative_to) - track_paths.append(item_path) - else: - self._log.warn(u"{} | track not found ({})", name, query) - # write playlist - playlist_dir = self.config['playlist_dir'].as_filename() - playlist_dir = bytestring_path(playlist_dir) - m3u_path = normpath(os.path.join(playlist_dir, bytestring_path(name))) - mkdirall(m3u_path) - with open(syspath(m3u_path), 'wb') as f: - for path in track_paths: - if self.config['forward_slash'].get(): - path = path_as_posix(path) - f.write(path + b'\n') - - def get_playlist_by_id(self, playlist_id, lib): - xml = self.send('getPlaylist', '&id={}'.format(playlist_id)).text - self.create_playlist(xml, lib) + return name, tracks def commands(self): def build_playlist(lib, opts, args): - - if len(self.config['playlist_ids'].as_str_seq()) > 0: - for playlist_id in self.config['playlist_ids'].as_str_seq(): - self.get_playlist_by_id(playlist_id, lib) + ids = self.config['playlist_ids'].as_str_seq() if len(self.config['playlist_names'].as_str_seq()) > 0: - playlists = ET.fromstring(self.send('getPlaylists').text)[0] + playlists = ET.fromstring(self.send('getPlaylists').text)[ + 0] if playlists.attrib.get('code', '200') != '200': alt_error = 'error getting playlists,' \ ' but no erro message found' - self._log.warn(playlists.attrib.get('message', alt_error)) + self._log.warn( + playlists.attrib.get('message', alt_error)) return for name in self.config['playlist_names'].as_str_seq(): for playlist in playlists: if name == playlist.attrib['name']: - self.get_playlist_by_id(playlist.attrib['id'], lib) + ids.append(playlist.attrib['id']) + playlist_dict = dict() + for playlist_id in ids: + name, tracks = self.get_playlist(playlist_id) + for track in tracks: + if track not in playlist_dict: + playlist_dict[track] = ';' + playlist_dict[track] += name + ';' + self.update_tags(playlist_dict, lib) subsonicplaylist_cmds = Subcommand( 'subsonicplaylist', help=u'import a subsonic playlist' From e9dee5dca83a02c9985d3fff61f18d9a83aea8c6 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sat, 16 Nov 2019 20:30:53 +0100 Subject: [PATCH 010/293] added flag to delete old tags --- beetsplug/subsonicplaylist.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 1dd73f939..ef100c6da 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -34,6 +34,7 @@ class SubsonicPlaylistPlugin(BeetsPlugin): super(SubsonicPlaylistPlugin, self).__init__() self.config.add( { + 'delete': False, 'playlist_ids': [], 'playlist_names': [], 'username': '', @@ -70,6 +71,7 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def commands(self): def build_playlist(lib, opts, args): + self._parse_opts(opts) ids = self.config['playlist_ids'].as_str_seq() if len(self.config['playlist_names'].as_str_seq()) > 0: playlists = ET.fromstring(self.send('getPlaylists').text)[ @@ -91,14 +93,35 @@ class SubsonicPlaylistPlugin(BeetsPlugin): if track not in playlist_dict: playlist_dict[track] = ';' playlist_dict[track] += name + ';' + # delete old tags + if self.config['delete']: + for item in lib.items('subsonic_playlist:";"'): + item.update({'subsonic_playlist': ''}) + with lib.transaction(): + item.try_sync(write=True, move=False) + self.update_tags(playlist_dict, lib) subsonicplaylist_cmds = Subcommand( 'subsonicplaylist', help=u'import a subsonic playlist' ) + subsonicplaylist_cmds.parser.add_option( + u'-d', + u'--delete', + action='store_true', + help=u'delete tag from items not in any playlist anymore', + ) subsonicplaylist_cmds.func = build_playlist return [subsonicplaylist_cmds] + def _parse_opts(self, opts): + + if opts.delete: + self.config['delete'].set(True) + + self.opts = opts + return True + def generate_token(self): salt = ''.join(random.choices(string.ascii_lowercase + string.digits)) return md5( From 00330da623be15eb7fd8f4ad19be65d7222dd945 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sat, 16 Nov 2019 21:10:41 +0100 Subject: [PATCH 011/293] updated documentation --- docs/plugins/subsonicplaylist.rst | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/plugins/subsonicplaylist.rst b/docs/plugins/subsonicplaylist.rst index d6d64f60b..22fe16816 100644 --- a/docs/plugins/subsonicplaylist.rst +++ b/docs/plugins/subsonicplaylist.rst @@ -1,21 +1,30 @@ Subsonic Playlist Plugin ======================== -The ``subsonicplaylist`` plugin allows to import playlist from a subsonic server +The ``subsonicplaylist`` plugin allows to import playlists from a subsonic server. +This is done by retrieving the track infos from the subsonic server, searching +them in the beets library and adding the playlist names to the +`subsonic_playlist` tag of the found items. The content of the tag has the format: + + subsonic_playlist: ";first playlist;second playlist" + +To get all items in a playlist use the query `;playlist name;`. Command Line Usage ------------------ To use the ``subsonicplaylist`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then use it by invoking the ``subsonicplaylist`` command. -The command will search the playlist on the subsonic server and create a playlist -using the beets library. Options to be defined in your config with their default value:: +By default only the tags of the items found for playlists will be updated. +This means that, if one imported a playlist, then delete one song from it and +imported the playlist again, the deleted song will still have the playlist set +in its `subsonic_playlist` tag. To solve this problem one can use the `-d/--delete` +flag. This resets all `subsonic_playlist` tag before importing playlists. +Options to be defined in your config with their default value:: subsonicplaylist: - base_url: "https://your.subsonic.server" - 'relative_to': None, - 'playlist_dir': '.', - 'forward_slash': False, + 'base_url': "https://your.subsonic.server" + 'delete': False, 'playlist_ids': [], 'playlist_names': [], 'username': '', From d43cf35ad233739bf397dfdc484cfedb779fe90a Mon Sep 17 00:00:00 2001 From: Xavier Hocquet Date: Thu, 5 Dec 2019 20:06:46 -0700 Subject: [PATCH 012/293] Strip and lowercase Genius lyrics artist comparison --- beetsplug/lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 16699d9d3..1345018f0 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -395,7 +395,7 @@ class Genius(Backend): song_info = None for hit in json["response"]["hits"]: - if hit["result"]["primary_artist"]["name"] == artist: + if hit["result"]["primary_artist"]["name"].strip(u'\u200b').lower() == artist.lower(): song_info = hit break From c8e8e587f8ffd8319e941a3e201dfd6a516d368e Mon Sep 17 00:00:00 2001 From: Xavier Hocquet Date: Thu, 5 Dec 2019 20:06:49 -0700 Subject: [PATCH 013/293] Add debug logger for Genius lyrics no-match --- beetsplug/lyrics.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 1345018f0..b10f8dd02 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -402,6 +402,8 @@ class Genius(Backend): if song_info: song_api_path = song_info["result"]["api_path"] return self.lyrics_from_song_api_path(song_api_path) + else: + self._log.debug(u'Genius did not return a matching artist entry') class LyricsWiki(SymbolsReplaced): From a1af5d280e56d51f2970997339582eae0305758b Mon Sep 17 00:00:00 2001 From: Tyler Faulk Date: Tue, 10 Dec 2019 22:54:31 -0500 Subject: [PATCH 014/293] added merge_environment_settings call in fetchart plugin to handle connections with proxy servers --- beetsplug/fetchart.py | 6 +++++- docs/changelog.rst | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index a815d4d9b..ca93d685e 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -164,13 +164,17 @@ def _logged_get(log, *args, **kwargs): message = 'getting URL' req = requests.Request('GET', *args, **req_kwargs) + with requests.Session() as s: s.headers = {'User-Agent': 'beets'} prepped = s.prepare_request(req) + settings = s.merge_environment_settings( + prepped.url, {}, None, None, None + ) + send_kwargs.update(settings) log.debug('{}: {}', message, prepped.url) return s.send(prepped, **send_kwargs) - class RequestMixin(object): """Adds a Requests wrapper to the class that uses the logger, which must be named `self._log`. diff --git a/docs/changelog.rst b/docs/changelog.rst index b5b6c3901..e24bb976e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -99,6 +99,9 @@ New features: Fixes: +* :doc:`/plugins/fetchart`: Fixed a bug that caused fetchart to not take + environment variables such as proxy servers into account when making requests + :bug:`3450` * :doc:`/plugins/inline`: In function-style field definitions that refer to flexible attributes, values could stick around from one function invocation to the next. This meant that, when displaying a list of objects, later From 6d69d01016adaa780d660ee3c16199ad2afb96f5 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Mon, 13 Jan 2020 15:42:55 +0100 Subject: [PATCH 015/293] added database changed event to subsonicplaylist --- beetsplug/subsonicplaylist.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index ef100c6da..fbbcd8ac3 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -20,6 +20,7 @@ import xml.etree.ElementTree as ET import random import string import requests +from beets import plugins from beets.ui import Subcommand @@ -101,6 +102,8 @@ class SubsonicPlaylistPlugin(BeetsPlugin): item.try_sync(write=True, move=False) self.update_tags(playlist_dict, lib) + ## notify plugins + plugins.send('database_change', lib=lib, model=self) subsonicplaylist_cmds = Subcommand( 'subsonicplaylist', help=u'import a subsonic playlist' From 53820c0a9843ce613feeb2d9896739f0d3cbf369 Mon Sep 17 00:00:00 2001 From: ybnd Date: Tue, 28 Jan 2020 09:45:20 +0100 Subject: [PATCH 016/293] Handle bs1770gain v0.6.0 XML output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove `0%\x08\x08` from output (backspace code doesn't resolve; progress percentages get spliced in) * Handle changed attributes/fields: * `sample-peak` attribute `factor` is called `amplitude` instead * Album summary is not included in a `summary` tag now, but in two separate `integrated` and `sample-peak` tags * Handle `lu` attribute * Get bs1770gain version * If v0.6.0 or later, add `--unit=ebu` flag to convert `db` attributes to LUFS * May be useful later on ### Output examples Track: ``` ``` Album: ``` ``` --- beetsplug/replaygain.py | 32 ++++++++++++++++++++++++++++++-- test/test_replaygain.py | 3 +-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 1076ac714..3e284ce62 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -22,6 +22,7 @@ import math import sys import warnings import enum +import re import xml.parsers.expat from six.moves import zip @@ -135,6 +136,11 @@ class Bs1770gainBackend(Backend): -18: "replaygain", } + version = re.search( + 'bs1770gain ([0-9]+.[0-9]+.[0-9]+), ', + call(['bs1770gain', '--version']).stdout.decode('utf-8') + ).group(1) + def __init__(self, config, log): super(Bs1770gainBackend, self).__init__(config, log) config.add({ @@ -252,6 +258,8 @@ class Bs1770gainBackend(Backend): cmd = [self.command] cmd += ["--" + method] cmd += ['--xml', '-p'] + if self.version >= '0.6.0': + cmd += ['--unit=ebu'] # set units to LU # Workaround for Windows: the underlying tool fails on paths # with the \\?\ prefix, so we don't use it here. This @@ -286,6 +294,7 @@ class Bs1770gainBackend(Backend): album_gain = {} # mutable variable so it can be set from handlers parser = xml.parsers.expat.ParserCreate(encoding='utf-8') state = {'file': None, 'gain': None, 'peak': None} + album_state = {'gain': None, 'peak': None} def start_element_handler(name, attrs): if name == u'track': @@ -294,9 +303,13 @@ class Bs1770gainBackend(Backend): raise ReplayGainError( u'duplicate filename in bs1770gain output') elif name == u'integrated': - state['gain'] = float(attrs[u'lu']) + if 'lu' in attrs: + state['gain'] = float(attrs[u'lu']) elif name == u'sample-peak': - state['peak'] = float(attrs[u'factor']) + if 'factor' in attrs: + state['peak'] = float(attrs[u'factor']) + elif 'amplitude' in attrs: + state['peak'] = float(attrs[u'amplitude']) def end_element_handler(name): if name == u'track': @@ -312,10 +325,25 @@ class Bs1770gainBackend(Backend): 'the output of bs1770gain') album_gain["album"] = Gain(state['gain'], state['peak']) state['gain'] = state['peak'] = None + elif len(per_file_gain) == len(path_list): + if state['gain'] is not None: + album_state['gain'] = state['gain'] + if state['peak'] is not None: + album_state['peak'] = state['peak'] + if album_state['gain'] is not None \ + and album_state['peak'] is not None: + album_gain["album"] = Gain( + album_state['gain'], album_state['peak']) + state['gain'] = state['peak'] = None + parser.StartElementHandler = start_element_handler parser.EndElementHandler = end_element_handler try: + if type(text) == bytes: + text = text.decode('utf-8') + while '\x08' in text: + text = re.sub('[^\x08]\x08', '', text) parser.Parse(text, True) except xml.parsers.expat.ExpatError: raise ReplayGainError( diff --git a/test/test_replaygain.py b/test/test_replaygain.py index fe0515bee..437b1426a 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -40,7 +40,7 @@ if any(has_program(cmd, ['-v']) for cmd in ['mp3gain', 'aacgain']): else: GAIN_PROG_AVAILABLE = False -if has_program('bs1770gain', ['--replaygain']): +if has_program('bs1770gain'): LOUDNESS_PROG_AVAILABLE = True else: LOUDNESS_PROG_AVAILABLE = False @@ -58,7 +58,6 @@ def reset_replaygain(item): class ReplayGainCliTestBase(TestHelper): - def setUp(self): self.setup_beets() self.config['replaygain']['backend'] = self.backend From c78afb1a97bb41788197ec1f7f3965e2e6a4f61b Mon Sep 17 00:00:00 2001 From: ybnd Date: Thu, 30 Jan 2020 16:37:56 +0100 Subject: [PATCH 017/293] Don't call bs1770gain outside of try statement --- beetsplug/replaygain.py | 12 +++++++----- test/test_replaygain.py | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 3e284ce62..5b715191e 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -136,11 +136,6 @@ class Bs1770gainBackend(Backend): -18: "replaygain", } - version = re.search( - 'bs1770gain ([0-9]+.[0-9]+.[0-9]+), ', - call(['bs1770gain', '--version']).stdout.decode('utf-8') - ).group(1) - def __init__(self, config, log): super(Bs1770gainBackend, self).__init__(config, log) config.add({ @@ -155,6 +150,13 @@ class Bs1770gainBackend(Backend): try: call([cmd, "--help"]) self.command = cmd + try: + self.version = re.search( + '([0-9]+.[0-9]+.[0-9]+), ', + call([cmd, '--version']).stdout.decode('utf-8') + ).group(1) + except AttributeError: + self.version = '0.0.0' except OSError: raise FatalReplayGainError( u'Is bs1770gain installed?' diff --git a/test/test_replaygain.py b/test/test_replaygain.py index 437b1426a..fe0515bee 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -40,7 +40,7 @@ if any(has_program(cmd, ['-v']) for cmd in ['mp3gain', 'aacgain']): else: GAIN_PROG_AVAILABLE = False -if has_program('bs1770gain'): +if has_program('bs1770gain', ['--replaygain']): LOUDNESS_PROG_AVAILABLE = True else: LOUDNESS_PROG_AVAILABLE = False @@ -58,6 +58,7 @@ def reset_replaygain(item): class ReplayGainCliTestBase(TestHelper): + def setUp(self): self.setup_beets() self.config['replaygain']['backend'] = self.backend From c1cb78c908b39d506aa6ea9384c1eb98938217cd Mon Sep 17 00:00:00 2001 From: ybnd Date: Thu, 30 Jan 2020 17:59:57 +0100 Subject: [PATCH 018/293] Small fixes in `replaygain.Bs1770gainBackend` and test_replaygain.py * Fix unspecified `gain_adjustment` when method defined in config * Fix difference between dB and LUFS values in case of mismatched `target_level`/`method`: ``` db_to_lufs( target_level ) - lufs_to_dB( -23 ) ``` * Ignore single assertion in case of bs1770gain (cherry picked from commit 2395bf224032c44f1ea5d28e0c63af96a92b96df) --- beetsplug/replaygain.py | 7 +++++-- test/test_replaygain.py | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 5b715191e..b86ef4b1e 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -249,12 +249,15 @@ class Bs1770gainBackend(Backend): if self.__method != "": # backward compatibility to `method` option method = self.__method + gain_adjustment = target_level \ + - [k for k, v in self.methods.items() if v == method][0] elif target_level in self.methods: method = self.methods[target_level] gain_adjustment = 0 else: - method = self.methods[-23] - gain_adjustment = target_level - lufs_to_db(-23) + lufs_target = -23 + method = self.methods[lufs_target] + gain_adjustment = target_level - lufs_target # Construct shell command. cmd = [self.command] diff --git a/test/test_replaygain.py b/test/test_replaygain.py index fe0515bee..3f317aeb3 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -151,7 +151,9 @@ class ReplayGainCliTestBase(TestHelper): self.assertEqual(max(gains), min(gains)) self.assertNotEqual(max(gains), 0.0) - self.assertNotEqual(max(peaks), 0.0) + if not self.backend == "bs1770gain": + # Actually produces peaks == 0.0 ~ self.add_album_fixture + self.assertNotEqual(max(peaks), 0.0) def test_cli_writes_only_r128_tags(self): if self.backend == "command": From c3817a4c06ea2e029a9d6a07a5f0e1facd6779b1 Mon Sep 17 00:00:00 2001 From: ybnd Date: Thu, 30 Jan 2020 18:13:59 +0100 Subject: [PATCH 019/293] Implement review comments * safer version comparison * regex bytes directly * handle b'\x08 ...' case * test_replaygain.py: injected command output should match the type of the actual output --- beetsplug/replaygain.py | 37 ++++++++++++++++++++++++------------- test/test_replaygain.py | 4 ++-- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index b86ef4b1e..661fcc2e3 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -25,6 +25,7 @@ import enum import re import xml.parsers.expat from six.moves import zip +from packaging import version from beets import ui from beets.plugins import BeetsPlugin @@ -148,19 +149,18 @@ class Bs1770gainBackend(Backend): cmd = 'bs1770gain' try: - call([cmd, "--help"]) + version_out = call([cmd, '--version']) self.command = cmd - try: - self.version = re.search( - '([0-9]+.[0-9]+.[0-9]+), ', - call([cmd, '--version']).stdout.decode('utf-8') - ).group(1) - except AttributeError: - self.version = '0.0.0' + self.version = re.search( + '([0-9]+.[0-9]+.[0-9]+), ', + version_out.stdout.decode('utf-8') + ).group(1) except OSError: raise FatalReplayGainError( u'Is bs1770gain installed?' ) + except AttributeError: + self.version = '0.0.0' if not self.command: raise FatalReplayGainError( u'no replaygain command found: install bs1770gain' @@ -263,7 +263,7 @@ class Bs1770gainBackend(Backend): cmd = [self.command] cmd += ["--" + method] cmd += ['--xml', '-p'] - if self.version >= '0.6.0': + if version.parse(self.version) >= version.parse('0.6.0'): cmd += ['--unit=ebu'] # set units to LU # Workaround for Windows: the underlying tool fails on paths @@ -345,10 +345,21 @@ class Bs1770gainBackend(Backend): parser.EndElementHandler = end_element_handler try: - if type(text) == bytes: - text = text.decode('utf-8') - while '\x08' in text: - text = re.sub('[^\x08]\x08', '', text) + # Sometimes, the XML out put of `bs1770gain` gets spliced with + # some progress percentages: b'9%\x08\x0810%\x08\x08' + # that are supposed to be canceled out by appending + # a b'\x08' backspace characters for every character, + # + # For some reason, these backspace characters don't get + # resolved, resulting in mangled XML. + + # While there are backspace characters in the output + while b'\x08' in text: + text = re.sub(b'[^\x08]\x08|^\x08', b'', text) + # Replace every occurence of a non-backspace character + # followed by a backspace character or a backspace character + # at the beginning of the string by an empty byte string b'' + parser.Parse(text, True) except xml.parsers.expat.ExpatError: raise ReplayGainError( diff --git a/test/test_replaygain.py b/test/test_replaygain.py index 3f317aeb3..3ac19f8f9 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -40,7 +40,7 @@ if any(has_program(cmd, ['-v']) for cmd in ['mp3gain', 'aacgain']): else: GAIN_PROG_AVAILABLE = False -if has_program('bs1770gain', ['--replaygain']): +if has_program('bs1770gain'): LOUDNESS_PROG_AVAILABLE = True else: LOUDNESS_PROG_AVAILABLE = False @@ -252,7 +252,7 @@ class ReplayGainLdnsCliMalformedTest(TestHelper, unittest.TestCase): @patch('beetsplug.replaygain.call') def test_malformed_output(self, call_patch): # Return malformed XML (the ampersand should be &) - call_patch.return_value = CommandOutput(stdout=""" + call_patch.return_value = CommandOutput(stdout=b""" From 506be0259789c552f346d618d1774847e57ce0cf Mon Sep 17 00:00:00 2001 From: ybnd Date: Thu, 30 Jan 2020 20:11:09 +0100 Subject: [PATCH 020/293] Remove `packaging` dependency --- beetsplug/replaygain.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 661fcc2e3..950969547 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -25,7 +25,6 @@ import enum import re import xml.parsers.expat from six.moves import zip -from packaging import version from beets import ui from beets.plugins import BeetsPlugin @@ -68,6 +67,11 @@ def call(args, **kwargs): raise ReplayGainError(u"argument encoding failed") +def after_version(version_a, version_b): + return tuple(int(s) for s in version_a.split('.')) \ + >= tuple(int(s) for s in version_b.split('.')) + + def db_to_lufs(db): """Convert db to LUFS. @@ -263,7 +267,7 @@ class Bs1770gainBackend(Backend): cmd = [self.command] cmd += ["--" + method] cmd += ['--xml', '-p'] - if version.parse(self.version) >= version.parse('0.6.0'): + if after_version(self.version, '0.6.0'): cmd += ['--unit=ebu'] # set units to LU # Workaround for Windows: the underlying tool fails on paths From bef473c8e85b375a2dc5a40feb82bc971286e550 Mon Sep 17 00:00:00 2001 From: ybnd Date: Fri, 31 Jan 2020 07:42:50 +0100 Subject: [PATCH 021/293] Remove spliced progress regex and add --suppress-progress flag --- beetsplug/replaygain.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 950969547..3c652c7bd 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -268,7 +268,8 @@ class Bs1770gainBackend(Backend): cmd += ["--" + method] cmd += ['--xml', '-p'] if after_version(self.version, '0.6.0'): - cmd += ['--unit=ebu'] # set units to LU + cmd += ['--unit=ebu'] # set units to LU + cmd += ['--suppress-progress'] # don't print % to XML output # Workaround for Windows: the underlying tool fails on paths # with the \\?\ prefix, so we don't use it here. This @@ -349,21 +350,6 @@ class Bs1770gainBackend(Backend): parser.EndElementHandler = end_element_handler try: - # Sometimes, the XML out put of `bs1770gain` gets spliced with - # some progress percentages: b'9%\x08\x0810%\x08\x08' - # that are supposed to be canceled out by appending - # a b'\x08' backspace characters for every character, - # - # For some reason, these backspace characters don't get - # resolved, resulting in mangled XML. - - # While there are backspace characters in the output - while b'\x08' in text: - text = re.sub(b'[^\x08]\x08|^\x08', b'', text) - # Replace every occurence of a non-backspace character - # followed by a backspace character or a backspace character - # at the beginning of the string by an empty byte string b'' - parser.Parse(text, True) except xml.parsers.expat.ExpatError: raise ReplayGainError( From 9f43408f1b25910f7ebe98e0a30aab6ff0a33611 Mon Sep 17 00:00:00 2001 From: Xavier Hocquet Date: Sun, 2 Feb 2020 15:57:43 -0700 Subject: [PATCH 022/293] Changelog and cleanup --- beetsplug/lyrics.py | 8 ++++++-- docs/changelog.rst | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index b10f8dd02..20e39548c 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -395,15 +395,19 @@ class Genius(Backend): song_info = None for hit in json["response"]["hits"]: - if hit["result"]["primary_artist"]["name"].strip(u'\u200b').lower() == artist.lower(): + # Genius uses zero-width characters to denote lowercase artist names + hit_artist = hit["result"]["primary_artist"]["name"].strip(u'\u200b').lower() + + if hit_artist == artist.lower(): song_info = hit break if song_info: + self._log.debug(u'fetched: {0}', song_info["result"]["url"]) song_api_path = song_info["result"]["api_path"] return self.lyrics_from_song_api_path(song_api_path) else: - self._log.debug(u'Genius did not return a matching artist entry') + self._log.debug(u'genius: no matching artist') class LyricsWiki(SymbolsReplaced): diff --git a/docs/changelog.rst b/docs/changelog.rst index c6b805b30..545bf7a84 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -134,6 +134,8 @@ Fixes: * :doc:`/plugins/bpd`: Fix the transition to next track when in consume mode. Thanks to :user:`aereaux`. :bug:`3437` +* :doc:`/plugins/lyrics`: Fix a corner-case with Genius lowercase artist names + :bug:`3446` For plugin developers: From 95e0f54d7cea6c338efa47d16bc96c148970219f Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 2 Feb 2020 21:04:55 -0500 Subject: [PATCH 023/293] Fix too-long lines (#3448) --- beetsplug/lyrics.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 20e39548c..0e797d5a3 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -395,8 +395,10 @@ class Genius(Backend): song_info = None for hit in json["response"]["hits"]: - # Genius uses zero-width characters to denote lowercase artist names - hit_artist = hit["result"]["primary_artist"]["name"].strip(u'\u200b').lower() + # Genius uses zero-width characters to denote lowercase + # artist names. + hit_artist = hit["result"]["primary_artist"]["name"]. \ + strip(u'\u200b').lower() if hit_artist == artist.lower(): song_info = hit From 9465e933ba4d24dd409fe4de894da71808113179 Mon Sep 17 00:00:00 2001 From: ybnd Date: Wed, 5 Feb 2020 08:36:44 +0100 Subject: [PATCH 024/293] Add to changelog --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 95fde6888..ffbcf3b97 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -151,6 +151,8 @@ Fixes: * :doc:`/plugins/bpd`: Fix the transition to next track when in consume mode. Thanks to :user:`aereaux`. :bug:`3437` +* :doc:`/plugins/replaygain`: Support ``bs1770gain`` v0.6.0 and up + :bug:`3480` For plugin developers: From 7005691410549c65c680c66bd852a38ad725c981 Mon Sep 17 00:00:00 2001 From: ybnd Date: Wed, 5 Feb 2020 08:52:50 +0100 Subject: [PATCH 025/293] Add comment to clarify unexpected AttributeError handling --- beetsplug/replaygain.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 3c652c7bd..f5a4a2c55 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -164,6 +164,9 @@ class Bs1770gainBackend(Backend): u'Is bs1770gain installed?' ) except AttributeError: + # Raised by ReplayGainLdnsCliMalformedTest.test_malformed_output + # in test_replaygain.py; bs1770gain backend runs even though + # the bs1770gain command is not found test.helper.has_program self.version = '0.0.0' if not self.command: raise FatalReplayGainError( From 63ea17365a5dc75a9decbba3899bb4975df3989c Mon Sep 17 00:00:00 2001 From: ybnd Date: Wed, 5 Feb 2020 09:03:45 +0100 Subject: [PATCH 026/293] Modify patched stdout in test_malformed_output --- beetsplug/replaygain.py | 7 +------ test/test_replaygain.py | 4 +++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index f5a4a2c55..646e3acce 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -156,18 +156,13 @@ class Bs1770gainBackend(Backend): version_out = call([cmd, '--version']) self.command = cmd self.version = re.search( - '([0-9]+.[0-9]+.[0-9]+), ', + 'bs1770gain ([0-9]+.[0-9]+.[0-9]+), ', version_out.stdout.decode('utf-8') ).group(1) except OSError: raise FatalReplayGainError( u'Is bs1770gain installed?' ) - except AttributeError: - # Raised by ReplayGainLdnsCliMalformedTest.test_malformed_output - # in test_replaygain.py; bs1770gain backend runs even though - # the bs1770gain command is not found test.helper.has_program - self.version = '0.0.0' if not self.command: raise FatalReplayGainError( u'no replaygain command found: install bs1770gain' diff --git a/test/test_replaygain.py b/test/test_replaygain.py index 3ac19f8f9..969f5c230 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -230,7 +230,9 @@ class ReplayGainLdnsCliMalformedTest(TestHelper, unittest.TestCase): # Patch call to return nothing, bypassing the bs1770gain installation # check. - call_patch.return_value = CommandOutput(stdout=b"", stderr=b"") + call_patch.return_value = CommandOutput( + stdout=b'bs1770gain 0.0.0, ', stderr=b'' + ) try: self.load_plugins('replaygain') except Exception: From 964a6c2e63e56a9d04f1c34cc8b21511e0a11ba0 Mon Sep 17 00:00:00 2001 From: Tyler Faulk Date: Thu, 6 Feb 2020 12:10:38 -0500 Subject: [PATCH 027/293] restored whitespace to please style checker --- beetsplug/fetchart.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index ca93d685e..f78e6116a 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -175,6 +175,7 @@ def _logged_get(log, *args, **kwargs): log.debug('{}: {}', message, prepped.url) return s.send(prepped, **send_kwargs) + class RequestMixin(object): """Adds a Requests wrapper to the class that uses the logger, which must be named `self._log`. From 91be732bf467f9bc33a2455a6a2c5aff12db810d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 6 Feb 2020 22:18:15 -0500 Subject: [PATCH 028/293] Fix whitespace (#3453) --- beetsplug/fetchart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 408b947ca..ad96ece23 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -169,7 +169,7 @@ def _logged_get(log, *args, **kwargs): s.headers = {'User-Agent': 'beets'} prepped = s.prepare_request(req) settings = s.merge_environment_settings( - prepped.url, {}, None, None, None + prepped.url, {}, None, None, None ) send_kwargs.update(settings) log.debug('{}: {}', message, prepped.url) From d43d54e21cde97f57f19486925ab56b419254cc8 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 6 Feb 2020 22:22:54 -0500 Subject: [PATCH 029/293] Try to work around a Werkzeug change? --- beetsplug/web/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index f53fb3a95..21ff5d94e 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -169,7 +169,7 @@ class IdListConverter(BaseConverter): return ids def to_url(self, value): - return ','.join(value) + return ','.join(str(v) for v in value) class QueryConverter(PathConverter): From 9426ad73455657c616a116761e65debf898f8384 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 17 Feb 2020 09:40:30 +0100 Subject: [PATCH 030/293] Added beets-bpmanalyser to the list of 'other plugins' --- docs/plugins/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 7dc152bd1..955cc1405 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -299,6 +299,8 @@ Here are a few of the plugins written by the beets community: * `beets-mosaic`_ generates a montage of a mosiac from cover art. +* `beets-bpmanalyser`_ analyses songs and calculates tempo(bpm). + .. _beets-barcode: https://github.com/8h2a/beets-barcode .. _beets-check: https://github.com/geigerzaehler/beets-check .. _copyartifacts: https://github.com/sbarakat/beets-copyartifacts @@ -321,3 +323,4 @@ Here are a few of the plugins written by the beets community: .. _beets-ydl: https://github.com/vmassuchetto/beets-ydl .. _beet-summarize: https://github.com/steven-murray/beet-summarize .. _beets-mosaic: https://github.com/SusannaMaria/beets-mosaic +.. _beets-bpmanalyser: https://github.com/adamjakab/BeetsPluginBpmAnalyser From ddfb715e322c38cdf22019dacb96d81951e96ea7 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 17 Feb 2020 06:49:26 -0800 Subject: [PATCH 031/293] Style fixes for #3491 --- docs/plugins/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 955cc1405..125bdd934 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -299,7 +299,7 @@ Here are a few of the plugins written by the beets community: * `beets-mosaic`_ generates a montage of a mosiac from cover art. -* `beets-bpmanalyser`_ analyses songs and calculates tempo(bpm). +* `beets-bpmanalyser`_ analyses songs and calculates their tempo (BPM). .. _beets-barcode: https://github.com/8h2a/beets-barcode .. _beets-check: https://github.com/geigerzaehler/beets-check From 535af4bdb2ef7e9470798830e87a9ba4bad9217e Mon Sep 17 00:00:00 2001 From: Aidan Epstein Date: Mon, 17 Feb 2020 07:40:38 -0800 Subject: [PATCH 032/293] parentwork: Only call store when the metadata has changed. Otherwise, this rewrites all your files every time. --- beetsplug/parentwork.py | 11 ++++++----- docs/changelog.rst | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index eaa8abb30..d40254696 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -89,10 +89,11 @@ class ParentWorkPlugin(BeetsPlugin): write = ui.should_write() for item in lib.items(ui.decargs(args)): - self.find_work(item, force_parent) - item.store() - if write: - item.try_write() + changed = self.find_work(item, force_parent) + if changed: + item.store() + if write: + item.try_write() command = ui.Subcommand( 'parentwork', help=u'fetche parent works, composers and dates') @@ -198,7 +199,7 @@ add one at https://musicbrainz.org/recording/{}', item, item.mb_trackid) if work_date: item['work_date'] = work_date - ui.show_model_changes( + return ui.show_model_changes( item, fields=['parentwork', 'parentwork_disambig', 'mb_parentworkid', 'parent_composer', 'parent_composer_sort', 'work_date']) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4c91b422c..97cc7ca12 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -158,6 +158,8 @@ Fixes: :bug:`3446` * :doc:`/plugins/replaygain`: Support ``bs1770gain`` v0.6.0 and up :bug:`3480` +* :doc:`/plugins/parentwork`: Don't save tracks when nothing has changed. + :bug:`3492` For plugin developers: From 86946ad4b7d03cd78d308456eec193b068078628 Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Sun, 15 Dec 2019 16:37:21 -0500 Subject: [PATCH 033/293] Allow the quality to be set for embedded/fetched cover art --- beets/art.py | 14 +++++++------- beets/util/artresizer.py | 11 ++++++----- beetsplug/convert.py | 2 +- beetsplug/embedart.py | 12 +++++++----- beetsplug/fetchart.py | 2 +- beetsplug/thumbnails.py | 2 +- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/beets/art.py b/beets/art.py index e7a087a05..e48be4a57 100644 --- a/beets/art.py +++ b/beets/art.py @@ -50,7 +50,7 @@ def get_art(log, item): return mf.art -def embed_item(log, item, imagepath, maxwidth=None, itempath=None, +def embed_item(log, item, imagepath, maxwidth=None, quality=75, itempath=None, compare_threshold=0, ifempty=False, as_album=False, id3v23=None): """Embed an image into the item's media file. @@ -64,7 +64,7 @@ def embed_item(log, item, imagepath, maxwidth=None, itempath=None, log.info(u'media file already contained art') return if maxwidth and not as_album: - imagepath = resize_image(log, imagepath, maxwidth) + imagepath = resize_image(log, imagepath, maxwidth, quality) # Get the `Image` object from the file. try: @@ -84,7 +84,7 @@ def embed_item(log, item, imagepath, maxwidth=None, itempath=None, item.try_write(path=itempath, tags={'images': [image]}, id3v23=id3v23) -def embed_album(log, album, maxwidth=None, quiet=False, +def embed_album(log, album, maxwidth=None, quality=75, quiet=False, compare_threshold=0, ifempty=False): """Embed album art into all of the album's items. """ @@ -97,20 +97,20 @@ def embed_album(log, album, maxwidth=None, quiet=False, displayable_path(imagepath), album) return if maxwidth: - imagepath = resize_image(log, imagepath, maxwidth) + imagepath = resize_image(log, imagepath, maxwidth, quality) log.info(u'Embedding album art into {0}', album) for item in album.items(): - embed_item(log, item, imagepath, maxwidth, None, + embed_item(log, item, imagepath, maxwidth, quality, None, compare_threshold, ifempty, as_album=True) -def resize_image(log, imagepath, maxwidth): +def resize_image(log, imagepath, maxwidth, quality): """Returns path to an image resized to maxwidth. """ log.debug(u'Resizing album art to {0} pixels wide', maxwidth) - imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath)) + imagepath = ArtResizer.shared.resize(maxwidth, quality, syspath(imagepath)) return imagepath diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 99e28c0cc..3a6f58752 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -59,7 +59,7 @@ def temp_file_for(path): return util.bytestring_path(f.name) -def pil_resize(maxwidth, path_in, path_out=None): +def pil_resize(maxwidth, quality, path_in, path_out=None): """Resize using Python Imaging Library (PIL). Return the output path of resized image. """ @@ -72,7 +72,7 @@ def pil_resize(maxwidth, path_in, path_out=None): im = Image.open(util.syspath(path_in)) size = maxwidth, maxwidth im.thumbnail(size, Image.ANTIALIAS) - im.save(util.py3_path(path_out)) + im.save(util.py3_path(path_out), quality=quality) return path_out except IOError: log.error(u"PIL cannot create thumbnail for '{0}'", @@ -80,7 +80,7 @@ def pil_resize(maxwidth, path_in, path_out=None): return path_in -def im_resize(maxwidth, path_in, path_out=None): +def im_resize(maxwidth, quality, path_in, path_out=None): """Resize using ImageMagick. Use the ``magick`` program or ``convert`` on older versions. Return @@ -96,6 +96,7 @@ def im_resize(maxwidth, path_in, path_out=None): cmd = ArtResizer.shared.im_convert_cmd + \ [util.syspath(path_in, prefix=False), '-resize', '{0}x>'.format(maxwidth), + '-quality', '{0}x'.format(quality), util.syspath(path_out, prefix=False)] try: @@ -190,14 +191,14 @@ class ArtResizer(six.with_metaclass(Shareable, object)): self.im_convert_cmd = ['magick'] self.im_identify_cmd = ['magick', 'identify'] - def resize(self, maxwidth, path_in, path_out=None): + def resize(self, maxwidth, quality, path_in, path_out=None): """Manipulate an image file according to the method, returning a new path. For PIL or IMAGEMAGIC methods, resizes the image to a temporary file. For WEBPROXY, returns `path_in` unmodified. """ if self.local: func = BACKEND_FUNCS[self.method[0]] - return func(maxwidth, path_in, path_out) + return func(maxwidth, quality, path_in, path_out) else: return path_in diff --git a/beetsplug/convert.py b/beetsplug/convert.py index e7ac4f3ac..ab86800f6 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -422,7 +422,7 @@ class ConvertPlugin(BeetsPlugin): util.displayable_path(album.artpath), util.displayable_path(dest)) if not pretend: - ArtResizer.shared.resize(maxwidth, album.artpath, dest) + ArtResizer.shared.resize(maxwidth, 75, album.artpath, dest) else: if pretend: msg = 'ln' if hardlink else ('ln -s' if link else 'cp') diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 71681f024..e581a2ddf 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -59,7 +59,8 @@ class EmbedCoverArtPlugin(BeetsPlugin): 'auto': True, 'compare_threshold': 0, 'ifempty': False, - 'remove_art_file': False + 'remove_art_file': False, + 'quality': 95, }) if self.config['maxwidth'].get(int) and not ArtResizer.shared.local: @@ -86,6 +87,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): u"-y", u"--yes", action="store_true", help=u"skip confirmation" ) maxwidth = self.config['maxwidth'].get(int) + quality = self.config['quality'].get(int) compare_threshold = self.config['compare_threshold'].get(int) ifempty = self.config['ifempty'].get(bool) @@ -104,8 +106,8 @@ class EmbedCoverArtPlugin(BeetsPlugin): return for item in items: - art.embed_item(self._log, item, imagepath, maxwidth, None, - compare_threshold, ifempty) + art.embed_item(self._log, item, imagepath, maxwidth, + quality, None, compare_threshold, ifempty) else: albums = lib.albums(decargs(args)) @@ -114,8 +116,8 @@ class EmbedCoverArtPlugin(BeetsPlugin): return for album in albums: - art.embed_album(self._log, album, maxwidth, False, - compare_threshold, ifempty) + art.embed_album(self._log, album, maxwidth, quality, + False, compare_threshold, ifempty) self.remove_artfile(album) embed_cmd.func = embed_func diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index ad96ece23..99c592991 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -135,7 +135,7 @@ class Candidate(object): def resize(self, plugin): if plugin.maxwidth and self.check == self.CANDIDATE_DOWNSCALE: - self.path = ArtResizer.shared.resize(plugin.maxwidth, self.path) + self.path = ArtResizer.shared.resize(plugin.maxwidth, 75, self.path) def _logged_get(log, *args, **kwargs): diff --git a/beetsplug/thumbnails.py b/beetsplug/thumbnails.py index fe36fbd13..de949ea32 100644 --- a/beetsplug/thumbnails.py +++ b/beetsplug/thumbnails.py @@ -152,7 +152,7 @@ class ThumbnailsPlugin(BeetsPlugin): self._log.debug(u"{1}x{1} thumbnail for {0} exists and is " u"recent enough", album, size) return False - resized = ArtResizer.shared.resize(size, album.artpath, + resized = ArtResizer.shared.resize(size, 75, album.artpath, util.syspath(target)) self.add_tags(album, util.syspath(resized)) shutil.move(resized, target) From a30c90e6152ed13293267e5246119398e676fcad Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Mon, 17 Feb 2020 22:22:47 -0500 Subject: [PATCH 034/293] Fix one of the failing tests --- test/test_thumbnails.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_thumbnails.py b/test/test_thumbnails.py index dc03f06f7..0cf22d4a1 100644 --- a/test/test_thumbnails.py +++ b/test/test_thumbnails.py @@ -154,7 +154,7 @@ class ThumbnailsTest(unittest.TestCase, TestHelper): any_order=True) resize = mock_artresizer.shared.resize - resize.assert_called_once_with(12345, path_to_art, md5_file) + resize.assert_called_once_with(12345, 75, path_to_art, md5_file) plugin.add_tags.assert_called_once_with(album, resize.return_value) mock_shutils.move.assert_called_once_with(resize.return_value, md5_file) From 036202e1c5a0cae19397affa6eeace1cfed5ea6c Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Tue, 18 Feb 2020 14:50:57 -0500 Subject: [PATCH 035/293] Default quality to 0 which means don't specify From the ImageMagick docs: "The default is to use the estimated quality of your input image if it can be determined, otherwise 92." In order to get the original behaviour we need to conditional add the quality parameter to the `magick` call. The quality range can be anything from 1 to 100, which gives us the convenience of using 0 to specify no specific quality level. --- beets/art.py | 17 +++++++++-------- beets/util/artresizer.py | 22 +++++++++++++--------- beetsplug/convert.py | 2 +- beetsplug/embedart.py | 10 ++++++---- test/test_thumbnails.py | 2 +- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/beets/art.py b/beets/art.py index e48be4a57..b8b172594 100644 --- a/beets/art.py +++ b/beets/art.py @@ -50,9 +50,9 @@ def get_art(log, item): return mf.art -def embed_item(log, item, imagepath, maxwidth=None, quality=75, itempath=None, - compare_threshold=0, ifempty=False, as_album=False, - id3v23=None): +def embed_item(log, item, imagepath, maxwidth=None, itempath=None, + compare_threshold=0, ifempty=False, as_album=False, id3v23=None, + quality=0): """Embed an image into the item's media file. """ # Conditions and filters. @@ -84,8 +84,8 @@ def embed_item(log, item, imagepath, maxwidth=None, quality=75, itempath=None, item.try_write(path=itempath, tags={'images': [image]}, id3v23=id3v23) -def embed_album(log, album, maxwidth=None, quality=75, quiet=False, - compare_threshold=0, ifempty=False): +def embed_album(log, album, maxwidth=None, quiet=False, compare_threshold=0, + ifempty=False, quality=0): """Embed album art into all of the album's items. """ imagepath = album.artpath @@ -102,15 +102,16 @@ def embed_album(log, album, maxwidth=None, quality=75, quiet=False, log.info(u'Embedding album art into {0}', album) for item in album.items(): - embed_item(log, item, imagepath, maxwidth, quality, None, - compare_threshold, ifempty, as_album=True) + embed_item(log, item, imagepath, maxwidth, None, compare_threshold, + ifempty, as_album=True, quality=quality) def resize_image(log, imagepath, maxwidth, quality): """Returns path to an image resized to maxwidth. """ log.debug(u'Resizing album art to {0} pixels wide', maxwidth) - imagepath = ArtResizer.shared.resize(maxwidth, quality, syspath(imagepath)) + imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath), + quality=quality) return imagepath diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 3a6f58752..09d138322 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -59,7 +59,7 @@ def temp_file_for(path): return util.bytestring_path(f.name) -def pil_resize(maxwidth, quality, path_in, path_out=None): +def pil_resize(maxwidth, path_in, path_out=None, quality=0): """Resize using Python Imaging Library (PIL). Return the output path of resized image. """ @@ -80,7 +80,7 @@ def pil_resize(maxwidth, quality, path_in, path_out=None): return path_in -def im_resize(maxwidth, quality, path_in, path_out=None): +def im_resize(maxwidth, path_in, path_out=None, quality=0): """Resize using ImageMagick. Use the ``magick`` program or ``convert`` on older versions. Return @@ -93,11 +93,15 @@ def im_resize(maxwidth, quality, path_in, path_out=None): # "-resize WIDTHx>" shrinks images with the width larger # than the given width while maintaining the aspect ratio # with regards to the height. - cmd = ArtResizer.shared.im_convert_cmd + \ - [util.syspath(path_in, prefix=False), - '-resize', '{0}x>'.format(maxwidth), - '-quality', '{0}x'.format(quality), - util.syspath(path_out, prefix=False)] + cmd = ArtResizer.shared.im_convert_cmd + [ + util.syspath(path_in, prefix=False), + '-resize', '{0}x>'.format(maxwidth), + ] + + if quality > 0: + cmd += ['-quality', '{0}'.format(quality)] + + cmd.append(util.syspath(path_out, prefix=False)) try: util.command_output(cmd) @@ -191,14 +195,14 @@ class ArtResizer(six.with_metaclass(Shareable, object)): self.im_convert_cmd = ['magick'] self.im_identify_cmd = ['magick', 'identify'] - def resize(self, maxwidth, quality, path_in, path_out=None): + def resize(self, maxwidth, path_in, path_out=None, quality=0): """Manipulate an image file according to the method, returning a new path. For PIL or IMAGEMAGIC methods, resizes the image to a temporary file. For WEBPROXY, returns `path_in` unmodified. """ if self.local: func = BACKEND_FUNCS[self.method[0]] - return func(maxwidth, quality, path_in, path_out) + return func(maxwidth, path_in, path_out, quality=quality) else: return path_in diff --git a/beetsplug/convert.py b/beetsplug/convert.py index ab86800f6..e7ac4f3ac 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -422,7 +422,7 @@ class ConvertPlugin(BeetsPlugin): util.displayable_path(album.artpath), util.displayable_path(dest)) if not pretend: - ArtResizer.shared.resize(maxwidth, 75, album.artpath, dest) + ArtResizer.shared.resize(maxwidth, album.artpath, dest) else: if pretend: msg = 'ln' if hardlink else ('ln -s' if link else 'cp') diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index e581a2ddf..61a4d798f 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -60,7 +60,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): 'compare_threshold': 0, 'ifempty': False, 'remove_art_file': False, - 'quality': 95, + 'quality': 0, }) if self.config['maxwidth'].get(int) and not ArtResizer.shared.local: @@ -107,7 +107,8 @@ class EmbedCoverArtPlugin(BeetsPlugin): for item in items: art.embed_item(self._log, item, imagepath, maxwidth, - quality, None, compare_threshold, ifempty) + None, compare_threshold, ifempty, + quality=quality) else: albums = lib.albums(decargs(args)) @@ -116,8 +117,9 @@ class EmbedCoverArtPlugin(BeetsPlugin): return for album in albums: - art.embed_album(self._log, album, maxwidth, quality, - False, compare_threshold, ifempty) + art.embed_album(self._log, album, maxwidth, + False, compare_threshold, ifempty, + quality=quality) self.remove_artfile(album) embed_cmd.func = embed_func diff --git a/test/test_thumbnails.py b/test/test_thumbnails.py index 0cf22d4a1..dc03f06f7 100644 --- a/test/test_thumbnails.py +++ b/test/test_thumbnails.py @@ -154,7 +154,7 @@ class ThumbnailsTest(unittest.TestCase, TestHelper): any_order=True) resize = mock_artresizer.shared.resize - resize.assert_called_once_with(12345, 75, path_to_art, md5_file) + resize.assert_called_once_with(12345, path_to_art, md5_file) plugin.add_tags.assert_called_once_with(album, resize.return_value) mock_shutils.move.assert_called_once_with(resize.return_value, md5_file) From 96b0e8a33ef0e9f38a4d6264cb4355434e7b17e5 Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Tue, 18 Feb 2020 16:44:45 -0500 Subject: [PATCH 036/293] No longer need to pass a default quality here --- beetsplug/fetchart.py | 2 +- beetsplug/thumbnails.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 99c592991..ad96ece23 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -135,7 +135,7 @@ class Candidate(object): def resize(self, plugin): if plugin.maxwidth and self.check == self.CANDIDATE_DOWNSCALE: - self.path = ArtResizer.shared.resize(plugin.maxwidth, 75, self.path) + self.path = ArtResizer.shared.resize(plugin.maxwidth, self.path) def _logged_get(log, *args, **kwargs): diff --git a/beetsplug/thumbnails.py b/beetsplug/thumbnails.py index de949ea32..fe36fbd13 100644 --- a/beetsplug/thumbnails.py +++ b/beetsplug/thumbnails.py @@ -152,7 +152,7 @@ class ThumbnailsPlugin(BeetsPlugin): self._log.debug(u"{1}x{1} thumbnail for {0} exists and is " u"recent enough", album, size) return False - resized = ArtResizer.shared.resize(size, 75, album.artpath, + resized = ArtResizer.shared.resize(size, album.artpath, util.syspath(target)) self.add_tags(album, util.syspath(resized)) shutil.move(resized, target) From c9e6f910308a4fa46b74755cb0ba2e373e117df2 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Thu, 20 Feb 2020 18:55:56 +0100 Subject: [PATCH 037/293] Added beets-goingrunning to the list of 'other plugins' --- docs/plugins/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 125bdd934..5d6f4795e 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -301,6 +301,8 @@ Here are a few of the plugins written by the beets community: * `beets-bpmanalyser`_ analyses songs and calculates their tempo (BPM). +* `beets-goingrunning`_ copies songs to external device based on their tempo (BPM) and length to go with your running session. + .. _beets-barcode: https://github.com/8h2a/beets-barcode .. _beets-check: https://github.com/geigerzaehler/beets-check .. _copyartifacts: https://github.com/sbarakat/beets-copyartifacts @@ -324,3 +326,4 @@ Here are a few of the plugins written by the beets community: .. _beet-summarize: https://github.com/steven-murray/beet-summarize .. _beets-mosaic: https://github.com/SusannaMaria/beets-mosaic .. _beets-bpmanalyser: https://github.com/adamjakab/BeetsPluginBpmAnalyser +.. _beets-goingrunning: https://pypi.org/project/beets-goingrunning/ \ No newline at end of file From 37f3e226b0fd8b044a8fff73a848a40107595e62 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Thu, 20 Feb 2020 18:58:11 +0100 Subject: [PATCH 038/293] beets-goingrunning - shortened description --- docs/plugins/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 5d6f4795e..383466a68 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -301,7 +301,7 @@ Here are a few of the plugins written by the beets community: * `beets-bpmanalyser`_ analyses songs and calculates their tempo (BPM). -* `beets-goingrunning`_ copies songs to external device based on their tempo (BPM) and length to go with your running session. +* `beets-goingrunning`_ copies songs to external device to go with your running session. .. _beets-barcode: https://github.com/8h2a/beets-barcode .. _beets-check: https://github.com/geigerzaehler/beets-check From bc53695f24d06bcc33df2d0a4e6e6a739cab27fb Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sat, 22 Feb 2020 01:56:57 +0100 Subject: [PATCH 039/293] added missing test requirement --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 544721937..c50e65bf5 100755 --- a/setup.py +++ b/setup.py @@ -118,7 +118,8 @@ setup( 'responses', 'pyxdg', 'python-mpd2', - 'discogs-client' + 'discogs-client', + 'requests_oauthlib' ] + ( # Tests for the thumbnails plugin need pathlib on Python 2 too. ['pathlib'] if (sys.version_info < (3, 4, 0)) else [] From 15ad525e0d904dab48d2575f5c2438b3f7d1276f Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Sat, 22 Feb 2020 12:02:10 -0500 Subject: [PATCH 040/293] Mention quality level in docstrings --- beets/art.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beets/art.py b/beets/art.py index b8b172594..0cb9cdf1b 100644 --- a/beets/art.py +++ b/beets/art.py @@ -107,9 +107,11 @@ def embed_album(log, album, maxwidth=None, quiet=False, compare_threshold=0, def resize_image(log, imagepath, maxwidth, quality): - """Returns path to an image resized to maxwidth. + """Returns path to an image resized to maxwidth and encoded with the + specified quality level. """ - log.debug(u'Resizing album art to {0} pixels wide', maxwidth) + log.debug(u'Resizing album art to {0} pixels wide and encoding at quality + level {1}', maxwidth, quality) imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath), quality=quality) return imagepath From d82573f8f3231863cb892d222250bd603a0fe382 Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Sat, 22 Feb 2020 12:02:20 -0500 Subject: [PATCH 041/293] Document quality setting --- docs/plugins/embedart.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/plugins/embedart.rst b/docs/plugins/embedart.rst index cc2fe6fc8..bd87abed6 100644 --- a/docs/plugins/embedart.rst +++ b/docs/plugins/embedart.rst @@ -58,6 +58,11 @@ file. The available options are: the aspect ratio is preserved. See also :ref:`image-resizing` for further caveats about image resizing. Default: 0 (disabled). +- **quality**: The quality level to use when encoding the image file when + downscaling to ``maxwidth``. The default behaviour depends on the method used + to scale the images. ImageMagick tries to estimate the input image quality and + uses 92 if it cannot be determined. Pillow defaults to 75. + Default: 0 (disabled) - **remove_art_file**: Automatically remove the album art file for the album after it has been embedded. This option is best used alongside the :doc:`FetchArt ` plugin to download art with the purpose of From 4e7fd47a3574e567482bc2fb7e9a3bb51215fc12 Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Sat, 22 Feb 2020 12:05:02 -0500 Subject: [PATCH 042/293] Add quality to art resizer docstring --- beets/util/artresizer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 09d138322..cf457e797 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -198,7 +198,8 @@ class ArtResizer(six.with_metaclass(Shareable, object)): def resize(self, maxwidth, path_in, path_out=None, quality=0): """Manipulate an image file according to the method, returning a new path. For PIL or IMAGEMAGIC methods, resizes the image to a - temporary file. For WEBPROXY, returns `path_in` unmodified. + temporary file and encodes with the specified quality level. + For WEBPROXY, returns `path_in` unmodified. """ if self.local: func = BACKEND_FUNCS[self.method[0]] From a729bd872959232dad22591bfa497b40f0974872 Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Sat, 22 Feb 2020 12:23:02 -0500 Subject: [PATCH 043/293] Send quality parameter to images.weserv.nl --- beets/util/artresizer.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index cf457e797..aee06a5b6 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -40,14 +40,19 @@ else: log = logging.getLogger('beets') -def resize_url(url, maxwidth): +def resize_url(url, maxwidth, quality=0): """Return a proxied image URL that resizes the original image to maxwidth (preserving aspect ratio). """ - return '{0}?{1}'.format(PROXY_URL, urlencode({ + params = { 'url': url.replace('http://', ''), 'w': maxwidth, - })) + } + + if quality > 0: + params['q'] = quality + + return '{0}?{1}'.format(PROXY_URL, urlencode(params)) def temp_file_for(path): @@ -215,7 +220,7 @@ class ArtResizer(six.with_metaclass(Shareable, object)): if self.local: return url else: - return resize_url(url, maxwidth) + return resize_url(url, maxwidth, quality) @property def local(self): From 6a84949020d0b05be86c0da56a13c67222c6f3ee Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Sat, 22 Feb 2020 12:43:22 -0500 Subject: [PATCH 044/293] Woops, this needs to be explicitly multiline --- beets/art.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/art.py b/beets/art.py index 0cb9cdf1b..20b0e96d2 100644 --- a/beets/art.py +++ b/beets/art.py @@ -110,7 +110,7 @@ def resize_image(log, imagepath, maxwidth, quality): """Returns path to an image resized to maxwidth and encoded with the specified quality level. """ - log.debug(u'Resizing album art to {0} pixels wide and encoding at quality + log.debug(u'Resizing album art to {0} pixels wide and encoding at quality \ level {1}', maxwidth, quality) imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath), quality=quality) From 0af1bf5fbbe9be452cd18cb6ad34d456739623cc Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Sat, 22 Feb 2020 13:00:21 -0500 Subject: [PATCH 045/293] Pass a default quality in here --- beets/util/artresizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index aee06a5b6..8f14c8baf 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -212,7 +212,7 @@ class ArtResizer(six.with_metaclass(Shareable, object)): else: return path_in - def proxy_url(self, maxwidth, url): + def proxy_url(self, maxwidth, url, quality=0): """Modifies an image URL according the method, returning a new URL. For WEBPROXY, a URL on the proxy server is returned. Otherwise, the URL is returned unmodified. From fe8ba17ced235df664d66b644c4bf5f05a816288 Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Sat, 22 Feb 2020 13:36:35 -0500 Subject: [PATCH 046/293] Add quality setting to fetchart plugin --- beetsplug/fetchart.py | 12 ++++++++---- docs/plugins/fetchart.rst | 3 +++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index ad96ece23..2fe8b0b2c 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -135,7 +135,8 @@ class Candidate(object): def resize(self, plugin): if plugin.maxwidth and self.check == self.CANDIDATE_DOWNSCALE: - self.path = ArtResizer.shared.resize(plugin.maxwidth, self.path) + self.path = ArtResizer.shared.resize(plugin.maxwidth, self.path, + quality=plugin.quality) def _logged_get(log, *args, **kwargs): @@ -777,6 +778,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): 'auto': True, 'minwidth': 0, 'maxwidth': 0, + 'quality': 0, 'enforce_ratio': False, 'cautious': False, 'cover_names': ['cover', 'front', 'art', 'album', 'folder'], @@ -793,6 +795,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): self.minwidth = self.config['minwidth'].get(int) self.maxwidth = self.config['maxwidth'].get(int) + self.quality = self.config['quality'].get(int) # allow both pixel and percentage-based margin specifications self.enforce_ratio = self.config['enforce_ratio'].get( @@ -922,9 +925,10 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): def art_for_album(self, album, paths, local_only=False): """Given an Album object, returns a path to downloaded art for the album (or None if no art is found). If `maxwidth`, then images are - resized to this maximum pixel size. If `local_only`, then only local - image files from the filesystem are returned; no network requests - are made. + resized to this maximum pixel size. If `quality` then resized images + are saved at the specified quality level. If `local_only`, then only + local image files from the filesystem are returned; no network + requests are made. """ out = None diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 68212a582..23362aee0 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -42,6 +42,9 @@ file. The available options are: - **maxwidth**: A maximum image width to downscale fetched images if they are too big. The resize operation reduces image width to at most ``maxwidth`` pixels. The height is recomputed so that the aspect ratio is preserved. +- **quality**: The quality level to use when encoding the image file when + downscaling to ``maxwidth``. + Default: 0 (disabled) - **enforce_ratio**: Only images with a width:height ratio of 1:1 are considered as valid album art candidates if set to ``yes``. It is also possible to specify a certain deviation to the exact ratio to From d2e32a6b2056ec4237dc327c38194a50c4b27893 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 23 Feb 2020 23:29:15 +0100 Subject: [PATCH 047/293] Raising error on missing configuration file inclusion --- beets/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beets/__init__.py b/beets/__init__.py index 20075073c..20aed95e3 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -35,6 +35,8 @@ class IncludeLazyConfig(confuse.LazyConfig): filename = view.as_filename() if os.path.isfile(filename): self.set_file(filename) + else: + raise FileNotFoundError("Warning! Configuration file({0}) does not exist!".format(filename)) except confuse.NotFoundError: pass From dfa45f62a59b7bc431af2084fe35d84c2c2d08bd Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 24 Feb 2020 00:07:24 +0100 Subject: [PATCH 048/293] fixed flake8 long line warning --- beets/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beets/__init__.py b/beets/__init__.py index 20aed95e3..c9863ee66 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -36,7 +36,8 @@ class IncludeLazyConfig(confuse.LazyConfig): if os.path.isfile(filename): self.set_file(filename) else: - raise FileNotFoundError("Warning! Configuration file({0}) does not exist!".format(filename)) + raise FileNotFoundError( + "Warning! Configuration file({0}) does not exist!".format(filename)) except confuse.NotFoundError: pass From c90f7aacfcb65eef3386c29d3c146f9f0a120ee1 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 24 Feb 2020 00:22:34 +0100 Subject: [PATCH 049/293] fixed flake8 long line warning (maybe) --- beets/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index c9863ee66..ed3af5588 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -36,8 +36,8 @@ class IncludeLazyConfig(confuse.LazyConfig): if os.path.isfile(filename): self.set_file(filename) else: - raise FileNotFoundError( - "Warning! Configuration file({0}) does not exist!".format(filename)) + raise FileNotFoundError("Warning! Configuration file({0}) " + "does not exist!".format(filename)) except confuse.NotFoundError: pass From 3db55c7bf4f25c59102b4884165d2956ae80cfba Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 24 Feb 2020 10:20:54 +0100 Subject: [PATCH 050/293] Simple warning on missing (included) configuration file. --- beets/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index ed3af5588..0b1048bdb 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -33,13 +33,12 @@ class IncludeLazyConfig(confuse.LazyConfig): try: for view in self['include']: filename = view.as_filename() - if os.path.isfile(filename): - self.set_file(filename) - else: - raise FileNotFoundError("Warning! Configuration file({0}) " - "does not exist!".format(filename)) + self.set_file(filename) except confuse.NotFoundError: pass + except confuse.ConfigReadError as err: + print("Warning! Missing configuration file! {}".format(err.reason)) + pass config = IncludeLazyConfig('beets', __name__) From 253d4c76d00f6fcf0e7cd6b30934aef0cf44212a Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 24 Feb 2020 10:43:03 +0100 Subject: [PATCH 051/293] removed redundant import and redundant filename variable --- beets/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index 0b1048bdb..bed59d05f 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -15,8 +15,6 @@ from __future__ import division, absolute_import, print_function -import os - import confuse __version__ = u'1.5.0' @@ -32,8 +30,7 @@ class IncludeLazyConfig(confuse.LazyConfig): try: for view in self['include']: - filename = view.as_filename() - self.set_file(filename) + self.set_file(view.as_filename()) except confuse.NotFoundError: pass except confuse.ConfigReadError as err: From 131227eff44b0fa596abfcd7780b6ea81cc50829 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 24 Feb 2020 22:38:30 +0100 Subject: [PATCH 052/293] writing warning message to stderr --- beets/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/beets/__init__.py b/beets/__init__.py index bed59d05f..e9cc10e08 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -16,6 +16,7 @@ from __future__ import division, absolute_import, print_function import confuse +from sys import stderr __version__ = u'1.5.0' __author__ = u'Adrian Sampson ' @@ -34,7 +35,8 @@ class IncludeLazyConfig(confuse.LazyConfig): except confuse.NotFoundError: pass except confuse.ConfigReadError as err: - print("Warning! Missing configuration file! {}".format(err.reason)) + stderr.write("Configuration 'import' failed: {}" + .format(err.reason)) pass From d06665413c5b871c9375c28f5544eb80e6cc0d1c Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Tue, 25 Feb 2020 15:23:52 +0100 Subject: [PATCH 053/293] minor fixes and changelog entry --- beets/__init__.py | 3 +-- docs/changelog.rst | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index e9cc10e08..e3e5fdf83 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -35,9 +35,8 @@ class IncludeLazyConfig(confuse.LazyConfig): except confuse.NotFoundError: pass except confuse.ConfigReadError as err: - stderr.write("Configuration 'import' failed: {}" + stderr.write("configuration `import` failed: {}" .format(err.reason)) - pass config = IncludeLazyConfig('beets', __name__) diff --git a/docs/changelog.rst b/docs/changelog.rst index 97cc7ca12..9a4ad0988 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -160,6 +160,8 @@ Fixes: :bug:`3480` * :doc:`/plugins/parentwork`: Don't save tracks when nothing has changed. :bug:`3492` +* Added a warning when configuration files defined in the `include` directive + of the configuration file fail to be imported. For plugin developers: From 20d28948a3cb51c0a4993d913bc716dc1a476e4a Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 25 Feb 2020 08:56:06 -0800 Subject: [PATCH 054/293] Changelog bug link for #3498 --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9a4ad0988..81ba590f8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -162,6 +162,7 @@ Fixes: :bug:`3492` * Added a warning when configuration files defined in the `include` directive of the configuration file fail to be imported. + :bug:`3498` For plugin developers: From a9e11fcfebbb80f3a0fe90664856a3398c7b5b6a Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Wed, 26 Feb 2020 19:23:59 -0500 Subject: [PATCH 055/293] Add some info about valid quality levels --- docs/plugins/embedart.rst | 9 ++++++--- docs/plugins/fetchart.rst | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/plugins/embedart.rst b/docs/plugins/embedart.rst index bd87abed6..ece5e0350 100644 --- a/docs/plugins/embedart.rst +++ b/docs/plugins/embedart.rst @@ -59,9 +59,12 @@ file. The available options are: caveats about image resizing. Default: 0 (disabled). - **quality**: The quality level to use when encoding the image file when - downscaling to ``maxwidth``. The default behaviour depends on the method used - to scale the images. ImageMagick tries to estimate the input image quality and - uses 92 if it cannot be determined. Pillow defaults to 75. + downscaling to ``maxwidth``. Can be a number from 1-100 or 0 to disable. + Higher numbers result in better image quality while lower numbers will result + in smaller files. 65-75 is a good starting point. The default behaviour + depends on the method used to scale the images. ImageMagick tries to estimate + the input image quality and uses 92 if it cannot be determined. Pillow + defaults to 75. Default: 0 (disabled) - **remove_art_file**: Automatically remove the album art file for the album after it has been embedded. This option is best used alongside the diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 23362aee0..8c5d0eb7b 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -43,7 +43,9 @@ file. The available options are: too big. The resize operation reduces image width to at most ``maxwidth`` pixels. The height is recomputed so that the aspect ratio is preserved. - **quality**: The quality level to use when encoding the image file when - downscaling to ``maxwidth``. + downscaling to ``maxwidth``. Can be a number from 1-100 or 0 to disable. + Higher numbers result in better image quality while lower numbers will result + in smaller files. 65-75 is a good starting point. Default: 0 (disabled) - **enforce_ratio**: Only images with a width:height ratio of 1:1 are considered as valid album art candidates if set to ``yes``. From b3fec61f5409b05fa2c31d19f5160e1526332fd4 Mon Sep 17 00:00:00 2001 From: Daniel Barber Date: Wed, 26 Feb 2020 19:31:42 -0500 Subject: [PATCH 056/293] Add details to changelog --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 97cc7ca12..dc09a404f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,9 @@ Changelog New features: +* :doc:`plugins/fetchart`: and :doc:`plugins/embedart`: Added a new ``quality`` + option that controls the quality of the image output when the image is + resized. * :doc:`plugins/keyfinder`: Added support for `keyfinder-cli`_ Thanks to :user:`BrainDamage`. * :doc:`plugins/fetchart`: Added a new ``high_resolution`` config option to From 00c6d1439ee29b3b4592d6f65cfa57b1d1f96575 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 26 Feb 2020 19:15:44 -0800 Subject: [PATCH 057/293] Docs tweaks for #3493 --- docs/changelog.rst | 2 +- docs/plugins/embedart.rst | 13 ++++++------- docs/plugins/fetchart.rst | 10 ++++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b16593f1d..7ef19871b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,7 +6,7 @@ Changelog New features: -* :doc:`plugins/fetchart`: and :doc:`plugins/embedart`: Added a new ``quality`` +* :doc:`plugins/fetchart` and :doc:`plugins/embedart`: Added a new ``quality`` option that controls the quality of the image output when the image is resized. * :doc:`plugins/keyfinder`: Added support for `keyfinder-cli`_ diff --git a/docs/plugins/embedart.rst b/docs/plugins/embedart.rst index ece5e0350..defd3fa4b 100644 --- a/docs/plugins/embedart.rst +++ b/docs/plugins/embedart.rst @@ -58,13 +58,12 @@ file. The available options are: the aspect ratio is preserved. See also :ref:`image-resizing` for further caveats about image resizing. Default: 0 (disabled). -- **quality**: The quality level to use when encoding the image file when - downscaling to ``maxwidth``. Can be a number from 1-100 or 0 to disable. - Higher numbers result in better image quality while lower numbers will result - in smaller files. 65-75 is a good starting point. The default behaviour - depends on the method used to scale the images. ImageMagick tries to estimate - the input image quality and uses 92 if it cannot be determined. Pillow - defaults to 75. +- **quality**: The JPEG quality level to use when compressing images (when + ``maxwidth`` is set). This should be either a number from 1 to 100 or 0 to + use the default quality. 65–75 is usually a good starting point. The default + behavior depends on the imaging tool used for scaling: ImageMagick tries to + estimate the input image quality and uses 92 if it cannot be determined, and + PIL defaults to 75. Default: 0 (disabled) - **remove_art_file**: Automatically remove the album art file for the album after it has been embedded. This option is best used alongside the diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 8c5d0eb7b..4441d4e30 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -42,10 +42,12 @@ file. The available options are: - **maxwidth**: A maximum image width to downscale fetched images if they are too big. The resize operation reduces image width to at most ``maxwidth`` pixels. The height is recomputed so that the aspect ratio is preserved. -- **quality**: The quality level to use when encoding the image file when - downscaling to ``maxwidth``. Can be a number from 1-100 or 0 to disable. - Higher numbers result in better image quality while lower numbers will result - in smaller files. 65-75 is a good starting point. +- **quality**: The JPEG quality level to use when compressing images (when + ``maxwidth`` is set). This should be either a number from 1 to 100 or 0 to + use the default quality. 65–75 is usually a good starting point. The default + behavior depends on the imaging tool used for scaling: ImageMagick tries to + estimate the input image quality and uses 92 if it cannot be determined, and + PIL defaults to 75. Default: 0 (disabled) - **enforce_ratio**: Only images with a width:height ratio of 1:1 are considered as valid album art candidates if set to ``yes``. From 594da1597a392a4917dab407198211bd23e8c2b2 Mon Sep 17 00:00:00 2001 From: smichel17 Date: Sun, 1 Mar 2020 11:45:14 -0500 Subject: [PATCH 058/293] Document running chroma plugin in verbose mode For #941 --- docs/plugins/chroma.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/plugins/chroma.rst b/docs/plugins/chroma.rst index 1b86073b8..58a51da2a 100644 --- a/docs/plugins/chroma.rst +++ b/docs/plugins/chroma.rst @@ -96,7 +96,9 @@ Usage Once you have all the dependencies sorted out, enable the ``chroma`` plugin in your configuration (see :ref:`using-plugins`) to benefit from fingerprinting -the next time you run ``beet import``. +the next time you run ``beet import``. The first time you do this, you may wish +to run in verbose mode (``beet -v import``) in order to verify that the +``chroma`` plugin is operational, since it does not show an indicator otherwise. You can also use the ``beet fingerprint`` command to generate fingerprints for items already in your library. (Provide a query to fingerprint a subset of your From fcc1951e7a104f4bc363070264a58278fa2b882d Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 2 Mar 2020 00:16:16 +0100 Subject: [PATCH 059/293] corrected typo whilst reading --- beets/dbcore/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 3195b52c9..b13f2638a 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -189,7 +189,7 @@ class LazyConvertDict(object): class Model(object): """An abstract object representing an object in the database. Model - objects act like dictionaries (i.e., the allow subscript access like + objects act like dictionaries (i.e., they allow subscript access like ``obj['field']``). The same field set is available via attribute access as a shortcut (i.e., ``obj.field``). Three kinds of attributes are available: From 545c65d903e38d37fd2c1734ec69eac609bea035 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 1 Mar 2020 19:35:23 -0500 Subject: [PATCH 060/293] Massage wording for #3504 --- docs/plugins/chroma.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/chroma.rst b/docs/plugins/chroma.rst index 58a51da2a..a6b60e6d8 100644 --- a/docs/plugins/chroma.rst +++ b/docs/plugins/chroma.rst @@ -96,9 +96,9 @@ Usage Once you have all the dependencies sorted out, enable the ``chroma`` plugin in your configuration (see :ref:`using-plugins`) to benefit from fingerprinting -the next time you run ``beet import``. The first time you do this, you may wish -to run in verbose mode (``beet -v import``) in order to verify that the -``chroma`` plugin is operational, since it does not show an indicator otherwise. +the next time you run ``beet import``. (The plugin doesn't produce any obvious +output by default. If you want to confirm that it's enabled, you can try +running in verbose mode once with ``beet -v import``.) You can also use the ``beet fingerprint`` command to generate fingerprints for items already in your library. (Provide a query to fingerprint a subset of your From d2d2b646c1bf26629663035b000b1a85ce949c56 Mon Sep 17 00:00:00 2001 From: jmwatte Date: Tue, 17 Nov 2015 12:37:15 +0100 Subject: [PATCH 061/293] Add plugin for Fish shell tab completion --- beetsplug/fish.py | 274 ++++++++++++++++++++++++++++++++++++++++++ docs/plugins/fish.rst | 43 +++++++ 2 files changed, 317 insertions(+) create mode 100644 beetsplug/fish.py create mode 100644 docs/plugins/fish.rst diff --git a/beetsplug/fish.py b/beetsplug/fish.py new file mode 100644 index 000000000..3a7682c2a --- /dev/null +++ b/beetsplug/fish.py @@ -0,0 +1,274 @@ +# This file is part of beets. +# Copyright 2015, winters jean-marie. +# +# 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: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""If you use the fish-shell http://fishshell.com/ ... this will do +autocomplete for you. It does the main commands and options for beet +and the plugins. +It gives you all the album and itemfields (like genre, album) but not all the +values for these. It suggest genre: or album: but not genre: Pop..Jazz...Rock +You can get that by specifying ex. --extravalues genre. +""" + +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +from beets.plugins import BeetsPlugin +from beets import library, ui +from beets.ui import commands +from operator import attrgetter +import os +BL_NEED2 = """complete -c beet -n '__fish_beet_needs_command' {} {}\n""" +BL_USE3 = """complete -c beet -n '__fish_beet_using_command {}' {} {}\n""" +BL_SUBS = """complete -c beet -n '__fish_at_level {} ""' {} {}\n""" +BL_EXTRA3 = """complete -c beet -n '__fish_beet_use_extra {}' {} {}\n""" + +HEAD = ''' +function __fish_beet_needs_command + set cmd (commandline -opc) + if test (count $cmd) -eq 1 + return 0 + end + return 1 +end + +function __fish_beet_using_command + set cmd (commandline -opc) + set needle (count $cmd) + if test $needle -gt 1 + if begin test $argv[1] = $cmd[2]; + and not contains -- $cmd[$needle] $FIELDS; end + return 0 + end + end + return 1 +end + +function __fish_beet_use_extra + set cmd (commandline -opc) + set needle (count $cmd) + if test $argv[2] = $cmd[$needle] + return 0 + end + return 1 +end +''' + + +class FishPlugin(BeetsPlugin): + + def commands(self): + cmd = ui.Subcommand('fish', help='make fish autocomplete beet') + cmd.func = self.run + cmd.parser.add_option('-f', '--noFields', action='store_true', + default=False, + help='no item/album fields for autocomplete') + cmd.parser.add_option( + '-e', + '--extravalues', + action='append', + type='choice', + choices=library.Item.all_keys() + + library.Album.all_keys(), + help='pick field, get field-values for autocomplete') + return [cmd] + + def run(self, lib, opts, args): + # we gather the commands from beet and from the plugins. + # we take the album and item fields. + # it wanted, we take the values from these fields. + # we make a giant string of tehm formatted in a way that + # allows fish to do autocompletion for beet. + homeDir = os.path.expanduser("~") + completePath = os.path.join(homeDir, '.config/fish/completions') + try: + os.makedirs(completePath) + except OSError: + if not os.path.isdir(completePath): + raise + pathAndFile = os.path.join(completePath, 'beet.fish') + nobasicfields = opts.noFields # do not complete for item/album fields + extravalues = opts.extravalues # ex complete all artist values + beetcmds = sorted( + (commands.default_commands + + commands.plugins.commands()), + key=attrgetter('name')) + fields = sorted(set( + library.Album.all_keys() + library.Item.all_keys())) + # collect cmds and their aliases and their help message + cmd_names_help = [] + for cmd in beetcmds: + names = ["\?" if alias == "?" else alias for alias in cmd.aliases] + names.append(cmd.name) + for name in names: + cmd_names_help.append((name, cmd.help)) + # here we go assembling the string + totstring = HEAD + "\n" + totstring += get_cmds_list([name[0] for name in cmd_names_help]) + totstring += '' if nobasicfields else get_standard_fields(fields) + totstring += get_extravalues(lib, extravalues) if extravalues else '' + totstring += "\n" + "# ====== {} =====".format( + "setup basic beet completion") + "\n" * 2 + totstring += get_basic_beet_options() + totstring += "\n" + "# ====== {} =====".format( + "setup field completion for subcommands") + "\n" + totstring += get_subcommands( + cmd_names_help, nobasicfields, extravalues) + # setup completion for all the command-options + totstring += get_all_commands(beetcmds) + + with open(pathAndFile, 'w') as fish_file: + fish_file.write(totstring) + + +def get_cmds_list(cmds_names): + # make list of all commands in beet&plugins + substr = '' + substr += ( + "set CMDS " + " ".join(cmds_names) + ("\n" * 2) + ) + return substr + + +def get_standard_fields(fields): + # make list of item/album fields & append with ':' + fields = (field + ":" for field in fields) + substr = '' + substr += ( + "set FIELDS " + " ".join(fields) + ("\n" * 2) + ) + return substr + + +def get_extravalues(lib, extravalues): + # make list of all values from a item/album field + # so type artist: and get completion for stones, beatles .. + word = '' + setOfValues = get_set_of_values_for_field(lib, extravalues) + for fld in extravalues: + extraname = fld.upper() + 'S' + word += ( + "set " + extraname + " " + " ".join(sorted(setOfValues[fld])) + + ("\n" * 2) + ) + return word + + +def get_set_of_values_for_field(lib, fields): + # get the unique values from a item/album field + dictOfFields = {} + for each in fields: + dictOfFields[each] = set() + for item in lib.items(): + for field in fields: + dictOfFields[field].add(wrap(item[field])) + return dictOfFields + + +def get_basic_beet_options(): + word = ( + BL_NEED2.format("-l format-item", + "-f -d 'print with custom format'") + + BL_NEED2.format("-l format-album", + "-f -d 'print with custom format'") + + BL_NEED2.format("-s l -l library", + "-f -r -d 'library database file to use'") + + BL_NEED2.format("-s d -l directory", + "-f -r -d 'destination music directory'") + + BL_NEED2.format("-s v -l verbose", + "-f -d 'print debugging information'") + + + BL_NEED2.format("-s c -l config", + "-f -r -d 'path to configuration file'") + + BL_NEED2.format("-s h -l help", + "-f -d 'print this help message and exit'")) + return word + + +def get_subcommands(cmd_name_and_help, nobasicfields, extravalues): + # formatting for fish to complete our fields/values + word = "" + for cmdname, cmdhelp in cmd_name_and_help: + word += "\n" + "# ------ {} -------".format( + "fieldsetups for " + cmdname) + "\n" + word += ( + BL_NEED2.format( + ("-a " + cmdname), + ("-f " + "-d " + wrap(clean_whitespace(cmdhelp))))) + + if nobasicfields is False: + word += ( + BL_USE3.format( + cmdname, + ("-a " + wrap("$FIELDS")), + ("-f " + "-d " + wrap("fieldname")))) + + if extravalues: + for f in extravalues: + setvar = wrap("$" + f.upper() + "S") + word += " ".join(BL_EXTRA3.format( + (cmdname + " " + f + ":"), + ('-f ' + '-A ' + '-a ' + setvar), + ('-d ' + wrap(f))).split()) + "\n" + return word + + +def get_all_commands(beetcmds): + # formatting for fish to complete command-options + word = "" + for cmd in beetcmds: + names = ["\?" if alias == "?" else alias for alias in cmd.aliases] + names.append(cmd.name) + for name in names: + word += "\n" + word += ("\n" * 2) + "# ====== {} =====".format( + "completions for " + name) + "\n" + + for option in cmd.parser._get_all_options()[1:]: + cmd_LO = (" -l " + option._long_opts[0].replace('--', '') + )if option._long_opts else '' + cmd_SO = (" -s " + option._short_opts[0].replace('-', '') + ) if option._short_opts else '' + cmd_needARG = ' -r ' if option.nargs in [1] else '' + cmd_helpstr = (" -d " + wrap(' '.join(option.help.split())) + ) if option.help else '' + cmd_arglist = (' -a ' + wrap(" ".join(option.choices)) + ) if option.choices else '' + + word += " ".join(BL_USE3.format( + name, + (cmd_needARG + cmd_SO + cmd_LO + " -f " + cmd_arglist), + cmd_helpstr).split()) + "\n" + + word = (word + " ".join(BL_USE3.format( + name, + ("-s " + "h " + "-l " + "help" + " -f "), + ('-d ' + wrap("print help") + "\n") + ).split())) + return word + + +def clean_whitespace(word): + # remove to much whitespace,tabs in string + return " ".join(word.split()) + + +def wrap(word): + # need " or ' around strings but watch out if they're in the string + sptoken = '\"' + if ('"') in word and ("'") in word: + word.replace('"', sptoken) + return '"' + word + '"' + + tok = '"' if "'" in word else "'" + return tok + word + tok diff --git a/docs/plugins/fish.rst b/docs/plugins/fish.rst new file mode 100644 index 000000000..a6e41a46d --- /dev/null +++ b/docs/plugins/fish.rst @@ -0,0 +1,43 @@ +Fish plugins +============ + +The ``fish`` plugin adds a ``beet fish`` command that will create a fish +autocompletion file ``beet.fish`` in ``~/.config/fish/completions`` +This makes `fish`_ - a different shell - autocomplete commands for beet. + +.. _fish: http://fishshell.com/ + +Configuring +=========== + +This will only make sense if you have the `fish`_ shell installed. +Enable the ``fish`` plugin (see :ref:`using-plugins`). +If you install or disable plugins, run ``beet fish`` again. It takes the values +from the plugins you have enabled. + +Using +===== + +Type ``beet fish``. Hit ``enter`` and will see the file ``beet.fish`` appear +in ``.config/fish/completions`` in your home folder. + +For a not-fish user: After you type ``beet`` in your fish-prompt and ``TAB`` +you will get the autosuggestions for all your plugins/commands and +typing ``-`` will get you all the options available to you. +If you type ``beet ls`` and you ``TAB`` you will get a list of all the album/item +fields that beet offers. Start typing ``genr`` ``TAB`` and fish completes +``genre:`` ... ready to type on... + +Options +======= + +The default is that you get autocompletion for all the album/item fields. +You can disable that with ``beet fish -f`` In that case you only get all +the plugins/commands/options. Everything else you type in yourself. +If you want completion for a specific album/item field, you can get that like +this ``beet fish -e genre`` or ``beet fish -e genre -e albumartist`` . +Then when you type at your fish-prompt ``beet list genre:`` and you ``TAB`` +you will get a list of all your genres to choose from. +REMEMBER : we get all the values of these fields and put them in the completion +file. It is not meant to be a replacement of your database. In other words : +speed and size matters. From b53a91662361f0434232e51a54c0fcef83351eac Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 2 Mar 2020 11:53:33 +0100 Subject: [PATCH 062/293] added normalize method to the Integer class --- beets/dbcore/types.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/beets/dbcore/types.py b/beets/dbcore/types.py index 521a5a1ee..5aa2b9812 100644 --- a/beets/dbcore/types.py +++ b/beets/dbcore/types.py @@ -131,6 +131,14 @@ class Integer(Type): query = query.NumericQuery model_type = int + def normalize(self, value): + try: + return self.model_type(round(float(value))) + except ValueError: + return self.null + except TypeError: + return self.null + class PaddedInt(Integer): """An integer field that is formatted with a given number of digits, From 5fc4d7c35e6d508b9084e1db518f2e24cbe3af28 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 2 Mar 2020 13:00:15 +0100 Subject: [PATCH 063/293] - added `field_two` as type STRING in ModelFixture1 - renamed test test_format_fixed_field to test_format_fixed_field_string - test_format_fixed_field_string not tests `field_two` with string values - added new test_format_fixed_field_integer to test field `field_one` as INTEGER - added new test_format_fixed_field_integer_normalized to test rounding float values --- test/test_dbcore.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/test/test_dbcore.py b/test/test_dbcore.py index 9bf78de67..0d40896da 100644 --- a/test/test_dbcore.py +++ b/test/test_dbcore.py @@ -53,6 +53,7 @@ class ModelFixture1(dbcore.Model): _fields = { 'id': dbcore.types.PRIMARY_ID, 'field_one': dbcore.types.INTEGER, + 'field_two': dbcore.types.STRING, } _types = { 'some_float_field': dbcore.types.FLOAT, @@ -355,7 +356,7 @@ class ModelTest(unittest.TestCase): def test_items(self): model = ModelFixture1(self.db) model.id = 5 - self.assertEqual({('id', 5), ('field_one', 0)}, + self.assertEqual({('id', 5), ('field_one', 0), ('field_two', '')}, set(model.items())) def test_delete_internal_field(self): @@ -370,10 +371,28 @@ class ModelTest(unittest.TestCase): class FormatTest(unittest.TestCase): - def test_format_fixed_field(self): + def test_format_fixed_field_integer(self): model = ModelFixture1() - model.field_one = u'caf\xe9' + model.field_one = 155 value = model.formatted().get('field_one') + self.assertEqual(value, u'155') + + def test_format_fixed_field_integer_normalized(self): + """The normalize method of the Integer class rounds floats + """ + model = ModelFixture1() + model.field_one = 142.432 + value = model.formatted().get('field_one') + self.assertEqual(value, u'142') + + model.field_one = 142.863 + value = model.formatted().get('field_one') + self.assertEqual(value, u'143') + + def test_format_fixed_field_string(self): + model = ModelFixture1() + model.field_two = u'caf\xe9' + value = model.formatted().get('field_two') self.assertEqual(value, u'caf\xe9') def test_format_flex_field(self): From 832c7326af8660e900988807ee2a1890cf24e67b Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 2 Mar 2020 13:03:18 +0100 Subject: [PATCH 064/293] corrected test to account for `year` and `disctotal` field now being treated as `types.INTEGER` --- test/test_autotag.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/test_autotag.py b/test/test_autotag.py index 28b7fd209..5ab3c4b49 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -21,6 +21,8 @@ import re import copy import unittest +from beets.dbcore.types import Integer, PaddedInt + from test import _common from beets import autotag from beets.autotag import match @@ -91,7 +93,10 @@ class PluralityTest(_common.TestCase): for i in range(5)] likelies, _ = match.current_metadata(items) for f in fields: - self.assertEqual(likelies[f], '%s_1' % f) + if type(items[0]._fields[f]) in (Integer, PaddedInt): + self.assertEqual(likelies[f], 0) + else: + self.assertEqual(likelies[f], '%s_1' % f) def _make_item(title, track, artist=u'some artist'): From 82c3867fc086e6729218c2884f494cfefbd11f56 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Mon, 2 Mar 2020 13:52:35 +0100 Subject: [PATCH 065/293] Rewrite Fish completion plugin docs & code comments --- beetsplug/fish.py | 58 ++++++++++++++++--------------- docs/plugins/fish.rst | 79 ++++++++++++++++++++++++------------------- 2 files changed, 74 insertions(+), 63 deletions(-) diff --git a/beetsplug/fish.py b/beetsplug/fish.py index 3a7682c2a..b81b9c387 100644 --- a/beetsplug/fish.py +++ b/beetsplug/fish.py @@ -1,5 +1,6 @@ # This file is part of beets. # Copyright 2015, winters jean-marie. +# Copyright 2020, Justin Mayer # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -12,12 +13,13 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""If you use the fish-shell http://fishshell.com/ ... this will do -autocomplete for you. It does the main commands and options for beet -and the plugins. -It gives you all the album and itemfields (like genre, album) but not all the -values for these. It suggest genre: or album: but not genre: Pop..Jazz...Rock -You can get that by specifying ex. --extravalues genre. +"""This plugin generates tab completions for Beets commands for the Fish shell +, including completions for Beets commands, plugin +commands, and option flags. Also generated are completions for all the album +and track fields, suggesting for example `genre:` or `album:` when querying the +Beets database. Completions for the *values* of those fields are not generated by +default but can be included via the `-e` or `--extravalues` flag. For example: +`beet fish -e genre -e albumartist` """ from __future__ import (division, absolute_import, print_function, @@ -68,11 +70,11 @@ end class FishPlugin(BeetsPlugin): def commands(self): - cmd = ui.Subcommand('fish', help='make fish autocomplete beet') + cmd = ui.Subcommand('fish', help='generate Fish shell tab completions') cmd.func = self.run cmd.parser.add_option('-f', '--noFields', action='store_true', default=False, - help='no item/album fields for autocomplete') + help='omit album/track field completions') cmd.parser.add_option( '-e', '--extravalues', @@ -80,15 +82,15 @@ class FishPlugin(BeetsPlugin): type='choice', choices=library.Item.all_keys() + library.Album.all_keys(), - help='pick field, get field-values for autocomplete') + help='include specified field *values* in completions') return [cmd] def run(self, lib, opts, args): - # we gather the commands from beet and from the plugins. - # we take the album and item fields. - # it wanted, we take the values from these fields. - # we make a giant string of tehm formatted in a way that - # allows fish to do autocompletion for beet. + # Gather the commands from Beets core and its plugins. + # Collect the album and track fields. + # If specified, also collect the values for these fields. + # Make a giant string of all the above, formatted in a way that + # allows Fish to do tab completion for the `beet` command. homeDir = os.path.expanduser("~") completePath = os.path.join(homeDir, '.config/fish/completions') try: @@ -97,22 +99,22 @@ class FishPlugin(BeetsPlugin): if not os.path.isdir(completePath): raise pathAndFile = os.path.join(completePath, 'beet.fish') - nobasicfields = opts.noFields # do not complete for item/album fields - extravalues = opts.extravalues # ex complete all artist values + nobasicfields = opts.noFields # Do not complete for album/track fields + extravalues = opts.extravalues # e.g., Also complete artists names beetcmds = sorted( (commands.default_commands + commands.plugins.commands()), key=attrgetter('name')) fields = sorted(set( library.Album.all_keys() + library.Item.all_keys())) - # collect cmds and their aliases and their help message + # Collect commands, their aliases, and their help text cmd_names_help = [] for cmd in beetcmds: names = ["\?" if alias == "?" else alias for alias in cmd.aliases] names.append(cmd.name) for name in names: cmd_names_help.append((name, cmd.help)) - # here we go assembling the string + # Concatenate the string totstring = HEAD + "\n" totstring += get_cmds_list([name[0] for name in cmd_names_help]) totstring += '' if nobasicfields else get_standard_fields(fields) @@ -124,7 +126,7 @@ class FishPlugin(BeetsPlugin): "setup field completion for subcommands") + "\n" totstring += get_subcommands( cmd_names_help, nobasicfields, extravalues) - # setup completion for all the command-options + # Set up completion for all the command options totstring += get_all_commands(beetcmds) with open(pathAndFile, 'w') as fish_file: @@ -132,7 +134,7 @@ class FishPlugin(BeetsPlugin): def get_cmds_list(cmds_names): - # make list of all commands in beet&plugins + # Make a list of all Beets core & plugin commands substr = '' substr += ( "set CMDS " + " ".join(cmds_names) + ("\n" * 2) @@ -141,7 +143,7 @@ def get_cmds_list(cmds_names): def get_standard_fields(fields): - # make list of item/album fields & append with ':' + # Make a list of album/track fields and append with ':' fields = (field + ":" for field in fields) substr = '' substr += ( @@ -151,8 +153,8 @@ def get_standard_fields(fields): def get_extravalues(lib, extravalues): - # make list of all values from a item/album field - # so type artist: and get completion for stones, beatles .. + # Make a list of all values from an album/track field. + # 'beet ls albumartist: ' yields completions for ABBA, Beatles, etc. word = '' setOfValues = get_set_of_values_for_field(lib, extravalues) for fld in extravalues: @@ -165,7 +167,7 @@ def get_extravalues(lib, extravalues): def get_set_of_values_for_field(lib, fields): - # get the unique values from a item/album field + # Get unique values from a specified album/track field dictOfFields = {} for each in fields: dictOfFields[each] = set() @@ -196,7 +198,7 @@ def get_basic_beet_options(): def get_subcommands(cmd_name_and_help, nobasicfields, extravalues): - # formatting for fish to complete our fields/values + # Formatting for Fish to complete our fields/values word = "" for cmdname, cmdhelp in cmd_name_and_help: word += "\n" + "# ------ {} -------".format( @@ -224,7 +226,7 @@ def get_subcommands(cmd_name_and_help, nobasicfields, extravalues): def get_all_commands(beetcmds): - # formatting for fish to complete command-options + # Formatting for Fish to complete command options word = "" for cmd in beetcmds: names = ["\?" if alias == "?" else alias for alias in cmd.aliases] @@ -259,12 +261,12 @@ def get_all_commands(beetcmds): def clean_whitespace(word): - # remove to much whitespace,tabs in string + # Remove excess whitespace and tabs in a string return " ".join(word.split()) def wrap(word): - # need " or ' around strings but watch out if they're in the string + # Need " or ' around strings but watch out if they're in the string sptoken = '\"' if ('"') in word and ("'") in word: word.replace('"', sptoken) diff --git a/docs/plugins/fish.rst b/docs/plugins/fish.rst index a6e41a46d..b2cb096ee 100644 --- a/docs/plugins/fish.rst +++ b/docs/plugins/fish.rst @@ -1,43 +1,52 @@ -Fish plugins -============ - -The ``fish`` plugin adds a ``beet fish`` command that will create a fish -autocompletion file ``beet.fish`` in ``~/.config/fish/completions`` -This makes `fish`_ - a different shell - autocomplete commands for beet. - -.. _fish: http://fishshell.com/ - -Configuring +Fish Plugin =========== -This will only make sense if you have the `fish`_ shell installed. -Enable the ``fish`` plugin (see :ref:`using-plugins`). -If you install or disable plugins, run ``beet fish`` again. It takes the values -from the plugins you have enabled. +The ``fish`` plugin adds a ``beet fish`` command that creates a `Fish shell`_ +tab-completion file named ``beet.fish`` in ``~/.config/fish/completions``. +This enables tab-completion of ``beet`` commands for the `Fish shell`_. -Using -===== +.. _Fish shell: https://fishshell.com/ -Type ``beet fish``. Hit ``enter`` and will see the file ``beet.fish`` appear -in ``.config/fish/completions`` in your home folder. +Configuration +------------- -For a not-fish user: After you type ``beet`` in your fish-prompt and ``TAB`` -you will get the autosuggestions for all your plugins/commands and -typing ``-`` will get you all the options available to you. -If you type ``beet ls`` and you ``TAB`` you will get a list of all the album/item -fields that beet offers. Start typing ``genr`` ``TAB`` and fish completes -``genre:`` ... ready to type on... +Enable the ``fish`` plugin (see :ref:`using-plugins`) on a system running the +`Fish shell`_. + +Usage +----- + +Type ``beet fish`` to generate the ``beet.fish`` completions file at: +``~/.config/fish/completions/``. If you later install or disable plugins, run +``beet fish`` again to update the completions based on the enabled plugins. + +For users not accustomed to tab completion… After you type ``beet`` followed by +a space in your shell prompt and then the ``TAB`` key, you should see a list of +the beets commands (and their abbreviated versions) that can be invoked in your +current environment. Similarly, typing ``beet -`` will show you all the +option flags available to you, which also applies to subcommands such as +``beet import -``. If you type ``beet ls`` followed by a space and then the +and the ``TAB`` key, you will see a list of all the album/track fields that can +be used in beets queries. For example, typing ``beet ls ge`` will complete +to ``genre:`` and leave you ready to type the rest of your query. Options -======= +------- -The default is that you get autocompletion for all the album/item fields. -You can disable that with ``beet fish -f`` In that case you only get all -the plugins/commands/options. Everything else you type in yourself. -If you want completion for a specific album/item field, you can get that like -this ``beet fish -e genre`` or ``beet fish -e genre -e albumartist`` . -Then when you type at your fish-prompt ``beet list genre:`` and you ``TAB`` -you will get a list of all your genres to choose from. -REMEMBER : we get all the values of these fields and put them in the completion -file. It is not meant to be a replacement of your database. In other words : -speed and size matters. +In addition to beets commands, plugin commands, and option flags, the generated +completions also include by default all the album/track fields. If you only want +the former and do not want the album/track fields included in the generated +completions, use ``beet fish -f`` to only generate completions for beets/plugin +commands and option flags. + +If you want generated completions to also contain album/track field *values* for +the items in your library, you can use the ``-e`` or ``--extravalues`` option. +For example: ``beet fish -e genre`` or ``beet fish -e genre -e albumartist`` +In the latter case, subsequently typing ``beet list genre: `` will display +a list of all the genres in your library and ``beet list albumartist: `` +will show a list of the album artists in your library. Keep in mind that all of +these values will be put into the generated completions file, so use this option +with care when specified fields contain a large number of values. Libraries with, +for example, very large numbers of genres/artists may result in higher memory +utilization, completion latency, et cetera. This option is not meant to replace +database queries altogether. From 05db0d18eb7f01f319bb30deb6d348d0ffe5150d Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Mon, 2 Mar 2020 15:38:56 +0100 Subject: [PATCH 066/293] Don't escape question marks in Fish completions Fish shell previously interpreted question marks as glob characters, but that behavior has been deprecated and will soon be removed. Plus, the completion for `help` and its alias `?` does not currently seem to behave as expected anyway and is thus, at present, of limited utility. --- beetsplug/fish.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/fish.py b/beetsplug/fish.py index b81b9c387..fd9753733 100644 --- a/beetsplug/fish.py +++ b/beetsplug/fish.py @@ -110,7 +110,7 @@ class FishPlugin(BeetsPlugin): # Collect commands, their aliases, and their help text cmd_names_help = [] for cmd in beetcmds: - names = ["\?" if alias == "?" else alias for alias in cmd.aliases] + names = [alias for alias in cmd.aliases] names.append(cmd.name) for name in names: cmd_names_help.append((name, cmd.help)) @@ -229,7 +229,7 @@ def get_all_commands(beetcmds): # Formatting for Fish to complete command options word = "" for cmd in beetcmds: - names = ["\?" if alias == "?" else alias for alias in cmd.aliases] + names = [alias for alias in cmd.aliases] names.append(cmd.name) for name in names: word += "\n" From f465c90e78d6bb102aec307057e37ad85e55422a Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Mon, 2 Mar 2020 15:46:04 +0100 Subject: [PATCH 067/293] Enforce PEP-8 compliance on Fish completion plugin --- beetsplug/fish.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/beetsplug/fish.py b/beetsplug/fish.py index fd9753733..b842ac70f 100644 --- a/beetsplug/fish.py +++ b/beetsplug/fish.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2015, winters jean-marie. # Copyright 2020, Justin Mayer @@ -17,13 +18,12 @@ , including completions for Beets commands, plugin commands, and option flags. Also generated are completions for all the album and track fields, suggesting for example `genre:` or `album:` when querying the -Beets database. Completions for the *values* of those fields are not generated by -default but can be included via the `-e` or `--extravalues` flag. For example: +Beets database. Completions for the *values* of those fields are not generated +by default but can be added via the `-e` / `--extravalues` flag. For example: `beet fish -e genre -e albumartist` """ -from __future__ import (division, absolute_import, print_function, - unicode_literals) +from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import library, ui @@ -91,14 +91,14 @@ class FishPlugin(BeetsPlugin): # If specified, also collect the values for these fields. # Make a giant string of all the above, formatted in a way that # allows Fish to do tab completion for the `beet` command. - homeDir = os.path.expanduser("~") - completePath = os.path.join(homeDir, '.config/fish/completions') + home_dir = os.path.expanduser("~") + completion_dir = os.path.join(home_dir, '.config/fish/completions') try: - os.makedirs(completePath) + os.makedirs(completion_dir) except OSError: - if not os.path.isdir(completePath): + if not os.path.isdir(completion_dir): raise - pathAndFile = os.path.join(completePath, 'beet.fish') + completion_file_path = os.path.join(completion_dir, 'beet.fish') nobasicfields = opts.noFields # Do not complete for album/track fields extravalues = opts.extravalues # e.g., Also complete artists names beetcmds = sorted( @@ -129,7 +129,7 @@ class FishPlugin(BeetsPlugin): # Set up completion for all the command options totstring += get_all_commands(beetcmds) - with open(pathAndFile, 'w') as fish_file: + with open(completion_file_path, 'w') as fish_file: fish_file.write(totstring) @@ -156,11 +156,11 @@ def get_extravalues(lib, extravalues): # Make a list of all values from an album/track field. # 'beet ls albumartist: ' yields completions for ABBA, Beatles, etc. word = '' - setOfValues = get_set_of_values_for_field(lib, extravalues) + values_set = get_set_of_values_for_field(lib, extravalues) for fld in extravalues: extraname = fld.upper() + 'S' word += ( - "set " + extraname + " " + " ".join(sorted(setOfValues[fld])) + "set " + extraname + " " + " ".join(sorted(values_set[fld])) + ("\n" * 2) ) return word @@ -168,13 +168,13 @@ def get_extravalues(lib, extravalues): def get_set_of_values_for_field(lib, fields): # Get unique values from a specified album/track field - dictOfFields = {} + fields_dict = {} for each in fields: - dictOfFields[each] = set() + fields_dict[each] = set() for item in lib.items(): for field in fields: - dictOfFields[field].add(wrap(item[field])) - return dictOfFields + fields_dict[field].add(wrap(item[field])) + return fields_dict def get_basic_beet_options(): @@ -237,11 +237,11 @@ def get_all_commands(beetcmds): "completions for " + name) + "\n" for option in cmd.parser._get_all_options()[1:]: - cmd_LO = (" -l " + option._long_opts[0].replace('--', '') - )if option._long_opts else '' - cmd_SO = (" -s " + option._short_opts[0].replace('-', '') - ) if option._short_opts else '' - cmd_needARG = ' -r ' if option.nargs in [1] else '' + cmd_l = (" -l " + option._long_opts[0].replace('--', '') + )if option._long_opts else '' + cmd_s = (" -s " + option._short_opts[0].replace('-', '') + ) if option._short_opts else '' + cmd_need_arg = ' -r ' if option.nargs in [1] else '' cmd_helpstr = (" -d " + wrap(' '.join(option.help.split())) ) if option.help else '' cmd_arglist = (' -a ' + wrap(" ".join(option.choices)) @@ -249,7 +249,7 @@ def get_all_commands(beetcmds): word += " ".join(BL_USE3.format( name, - (cmd_needARG + cmd_SO + cmd_LO + " -f " + cmd_arglist), + (cmd_need_arg + cmd_s + cmd_l + " -f " + cmd_arglist), cmd_helpstr).split()) + "\n" word = (word + " ".join(BL_USE3.format( From 14a654bbdbbeacf2c5792d112a0ef4eb03c06a10 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Mon, 2 Mar 2020 09:56:40 +0100 Subject: [PATCH 068/293] Add Fish completion to changelog & plugin index --- docs/changelog.rst | 2 ++ docs/plugins/index.rst | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7ef19871b..e33cf8c12 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog New features: +* A new :doc:`/plugins/fish` adds `Fish shell`_ tab autocompletion to beets * :doc:`plugins/fetchart` and :doc:`plugins/embedart`: Added a new ``quality`` option that controls the quality of the image output when the image is resized. @@ -217,6 +218,7 @@ For packagers: the test may no longer be necessary. * This version drops support for Python 3.4. +.. _Fish shell: https://fishshell.com/ .. _MediaFile: https://github.com/beetbox/mediafile .. _Confuse: https://github.com/beetbox/confuse .. _works: https://musicbrainz.org/doc/Work diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 383466a68..6c643ce61 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -78,6 +78,7 @@ following to your configuration:: export fetchart filefilter + fish freedesktop fromfilename ftintitle @@ -184,6 +185,7 @@ Interoperability * :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 @@ -203,6 +205,7 @@ Interoperability .. _Emby: https://emby.media +.. _Fish shell: https://fishshell.com/ .. _Plex: https://plex.tv .. _Kodi: https://kodi.tv .. _Sonos: https://sonos.com @@ -326,4 +329,4 @@ Here are a few of the plugins written by the beets community: .. _beet-summarize: https://github.com/steven-murray/beet-summarize .. _beets-mosaic: https://github.com/SusannaMaria/beets-mosaic .. _beets-bpmanalyser: https://github.com/adamjakab/BeetsPluginBpmAnalyser -.. _beets-goingrunning: https://pypi.org/project/beets-goingrunning/ \ No newline at end of file +.. _beets-goingrunning: https://pypi.org/project/beets-goingrunning/ From fbd1266bc5ed6c71dda030ef9313763c9039a376 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sat, 14 Mar 2020 21:30:22 +0100 Subject: [PATCH 069/293] simplified condition in test and added changelog entry --- docs/changelog.rst | 3 +++ test/test_autotag.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7ef19871b..fca3e686f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -166,6 +166,9 @@ Fixes: * Added a warning when configuration files defined in the `include` directive of the configuration file fail to be imported. :bug:`3498` +* Added the normalize method to the dbcore.types.INTEGER class which now + properly returns integer values. + :bug:`762` and :bug:`3507` For plugin developers: diff --git a/test/test_autotag.py b/test/test_autotag.py index 5ab3c4b49..f8f329024 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -93,7 +93,7 @@ class PluralityTest(_common.TestCase): for i in range(5)] likelies, _ = match.current_metadata(items) for f in fields: - if type(items[0]._fields[f]) in (Integer, PaddedInt): + if isinstance(likelies[f], int): self.assertEqual(likelies[f], 0) else: self.assertEqual(likelies[f], '%s_1' % f) From d5a52cbd26beee9f571fe6ea433b65bc75ee246d Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sat, 14 Mar 2020 21:51:46 +0100 Subject: [PATCH 070/293] removed unused imports --- test/test_autotag.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_autotag.py b/test/test_autotag.py index f8f329024..a11bc8fac 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -21,8 +21,6 @@ import re import copy import unittest -from beets.dbcore.types import Integer, PaddedInt - from test import _common from beets import autotag from beets.autotag import match From 99a3343c0c5860f4708d5272e5e1d680dc787ad6 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 14 Mar 2020 20:27:57 -0400 Subject: [PATCH 071/293] A little more detail in changelog for #3508 --- docs/changelog.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2f0997271..d3379b36c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -168,8 +168,9 @@ Fixes: of the configuration file fail to be imported. :bug:`3498` * Added the normalize method to the dbcore.types.INTEGER class which now - properly returns integer values. - :bug:`762` and :bug:`3507` + properly returns integer values, which should avoid problems where fields + like ``bpm`` would sometimes store non-integer values. + :bug:`762` :bug:`3507` :bug:`3508` For plugin developers: From 8988d908c5f39d6192fed61777bb37da464a20c8 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 12:52:12 +0100 Subject: [PATCH 072/293] match method converted to instance method --- beets/dbcore/query.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 8fb64e206..239eaa965 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -156,10 +156,9 @@ class NoneQuery(FieldQuery): def col_clause(self): return self.field + " IS NULL", () - @classmethod - def match(cls, item): + def match(self, item): try: - return item[cls.field] is None + return item.get(self.field) is None except KeyError: return True From eb4d2ef5c9655da307a238b7644ec00f25d3e6f9 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 12:52:56 +0100 Subject: [PATCH 073/293] added missing abstract method --- beets/dbcore/query.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 239eaa965..1bac90c06 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -162,6 +162,10 @@ class NoneQuery(FieldQuery): except KeyError: return True + @classmethod + def value_match(cls, pattern, value): + return pattern == value + def __repr__(self): return "{0.__class__.__name__}({0.field!r}, {0.fast})".format(self) From 93744ff00a10dc3ecba33545d41b8b0e57dbf3f5 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 14:40:40 +0100 Subject: [PATCH 074/293] removed unnecessary KeyError test --- beets/dbcore/query.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 1bac90c06..5835ddc32 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -157,10 +157,7 @@ class NoneQuery(FieldQuery): return self.field + " IS NULL", () def match(self, item): - try: - return item.get(self.field) is None - except KeyError: - return True + return item.get(self.field, default=None) is None @classmethod def value_match(cls, pattern, value): From d6538e5f0c0e7eae2b4e9fee8243a2724d3da066 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 14:57:02 +0100 Subject: [PATCH 075/293] removed value_match method - not reachable? --- beets/dbcore/query.py | 4 ---- test/test_query.py | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 5835ddc32..db4b861c4 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -159,10 +159,6 @@ class NoneQuery(FieldQuery): def match(self, item): return item.get(self.field, default=None) is None - @classmethod - def value_match(cls, pattern, value): - return pattern == value - def __repr__(self): return "{0.__class__.__name__}({0.field!r}, {0.fast})".format(self) diff --git a/test/test_query.py b/test/test_query.py index c0ab2a171..5e1556a45 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -772,6 +772,25 @@ class NoneQueryTest(unittest.TestCase, TestHelper): matched = self.lib.items(NoneQuery(u'rg_track_gain')) self.assertInResult(item, matched) + def test_match_slow(self): + item = self.add_item() + matched = self.lib.items(NoneQuery(u'rg_track_peak', fast=False)) + self.assertInResult(item, matched) + + def test_match_slow_after_set_none(self): + item = self.add_item(rg_track_gain=0) + matched = self.lib.items(NoneQuery(u'rg_track_gain', fast=False)) + self.assertNotInResult(item, matched) + + item['rg_track_gain'] = None + item.store() + matched = self.lib.items(NoneQuery(u'rg_track_gain', fast=False)) + self.assertInResult(item, matched) + + def test_match_repr(self): + q = NoneQuery(u'rg_track_gain', fast=False) + self.assertEquals("NoneQuery('rg_track_gain', False)", str(q)) + class NotQueryMatchTest(_common.TestCase): """Test `query.NotQuery` matching against a single item, using the same From 238f2244c91baaa0f5a9cb16c3ec6ca4ebb8a853 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 15:23:52 +0100 Subject: [PATCH 076/293] value_match is now correctly implemented --- beets/dbcore/query.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index db4b861c4..03b647a0d 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -152,12 +152,16 @@ class NoneQuery(FieldQuery): def __init__(self, field, fast=True): super(NoneQuery, self).__init__(field, None, fast) + self.pattern = None def col_clause(self): return self.field + " IS NULL", () def match(self, item): - return item.get(self.field, default=None) is None + return self.value_match(self.pattern, item.get(self.field, default=None)) + + def value_match(self, pattern, value): + return value is pattern def __repr__(self): return "{0.__class__.__name__}({0.field!r}, {0.fast})".format(self) From 3f2f125b090c20889bbeca28f72be4dd4ea85d3c Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 15:24:17 +0100 Subject: [PATCH 077/293] better repr testing --- test/test_query.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/test_query.py b/test/test_query.py index 5e1556a45..855dd8967 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -787,9 +787,11 @@ class NoneQueryTest(unittest.TestCase, TestHelper): matched = self.lib.items(NoneQuery(u'rg_track_gain', fast=False)) self.assertInResult(item, matched) - def test_match_repr(self): - q = NoneQuery(u'rg_track_gain', fast=False) - self.assertEquals("NoneQuery('rg_track_gain', False)", str(q)) + def test_query_repr(self): + fld = u'rg_track_gain' + self.assertEquals("NoneQuery('{}', True)".format(str(fld)), str(NoneQuery(fld))) + self.assertEquals("NoneQuery('{}', True)".format(str(fld)), str(NoneQuery(fld, fast=True))) + self.assertEquals("NoneQuery('{}', False)".format(str(fld)), str(NoneQuery(fld, fast=False))) class NotQueryMatchTest(_common.TestCase): From 532c6d7c825ae40a9091e32f5eb54863f591a72b Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 15:41:32 +0100 Subject: [PATCH 078/293] better repr testing #2 --- test/test_query.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/test_query.py b/test/test_query.py index 855dd8967..aa41497b7 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -788,10 +788,12 @@ class NoneQueryTest(unittest.TestCase, TestHelper): self.assertInResult(item, matched) def test_query_repr(self): - fld = u'rg_track_gain' - self.assertEquals("NoneQuery('{}', True)".format(str(fld)), str(NoneQuery(fld))) - self.assertEquals("NoneQuery('{}', True)".format(str(fld)), str(NoneQuery(fld, fast=True))) - self.assertEquals("NoneQuery('{}', False)".format(str(fld)), str(NoneQuery(fld, fast=False))) + self.assertEquals("NoneQuery('rg_track_gain', True)", + str(NoneQuery(u'rg_track_gain'))) + self.assertEquals("NoneQuery('rg_track_gain', True)", + str(NoneQuery(u'rg_track_gain', fast=True))) + self.assertEquals("NoneQuery('rg_track_gain', False)", + str(NoneQuery(u'rg_track_gain', fast=False))) class NotQueryMatchTest(_common.TestCase): From 8e68b5ff2e89dbd270b7493e8153cb3c90e2fd08 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 15:54:01 +0100 Subject: [PATCH 079/293] cleaning up --- beets/dbcore/query.py | 3 ++- test/test_query.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 03b647a0d..e4f5b8a29 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -158,7 +158,8 @@ class NoneQuery(FieldQuery): return self.field + " IS NULL", () def match(self, item): - return self.value_match(self.pattern, item.get(self.field, default=None)) + return self.value_match(self.pattern, + item.get(self.field, default=None)) def value_match(self, pattern, value): return value is pattern diff --git a/test/test_query.py b/test/test_query.py index aa41497b7..10206eb98 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -788,11 +788,11 @@ class NoneQueryTest(unittest.TestCase, TestHelper): self.assertInResult(item, matched) def test_query_repr(self): - self.assertEquals("NoneQuery('rg_track_gain', True)", + self.assertEquals("NoneQuery(u'rg_track_gain', True)", str(NoneQuery(u'rg_track_gain'))) - self.assertEquals("NoneQuery('rg_track_gain', True)", + self.assertEquals("NoneQuery(u'rg_track_gain', True)", str(NoneQuery(u'rg_track_gain', fast=True))) - self.assertEquals("NoneQuery('rg_track_gain', False)", + self.assertEquals("NoneQuery(u'rg_track_gain', False)", str(NoneQuery(u'rg_track_gain', fast=False))) From 935768d98326c662e9d62add0fdb9aa05d9bb05f Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 19:19:09 +0100 Subject: [PATCH 080/293] fixing repr tests --- test/test_query.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/test/test_query.py b/test/test_query.py index 10206eb98..ad770e1b9 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -788,12 +788,21 @@ class NoneQueryTest(unittest.TestCase, TestHelper): self.assertInResult(item, matched) def test_query_repr(self): - self.assertEquals("NoneQuery(u'rg_track_gain', True)", - str(NoneQuery(u'rg_track_gain'))) - self.assertEquals("NoneQuery(u'rg_track_gain', True)", - str(NoneQuery(u'rg_track_gain', fast=True))) - self.assertEquals("NoneQuery(u'rg_track_gain', False)", - str(NoneQuery(u'rg_track_gain', fast=False))) + fld = u'rg_track_gain' + if sys.version_info <= (2, 7): + self.assertEquals("NoneQuery('u{}', True)".format(fld), + str(NoneQuery(fld))) + self.assertEquals("NoneQuery('u{}', True)".format(fld), + str(NoneQuery(fld, fast=True))) + self.assertEquals("NoneQuery('u{}', False)".format(fld), + str(NoneQuery(fld, fast=False))) + else: + self.assertEquals("NoneQuery('{}', True)".format(fld), + str(NoneQuery(fld))) + self.assertEquals("NoneQuery('{}', True)".format(fld), + str(NoneQuery(fld, fast=True))) + self.assertEquals("NoneQuery('{}', False)".format(fld), + str(NoneQuery(fld, fast=False))) class NotQueryMatchTest(_common.TestCase): From ceb901fcca8958c7930a79dab1830533833e477c Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 19:30:24 +0100 Subject: [PATCH 081/293] struggling with old python --- test/test_query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_query.py b/test/test_query.py index ad770e1b9..06f2deec2 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -789,7 +789,7 @@ class NoneQueryTest(unittest.TestCase, TestHelper): def test_query_repr(self): fld = u'rg_track_gain' - if sys.version_info <= (2, 7): + if sys.version_info <= (3, 0): self.assertEquals("NoneQuery('u{}', True)".format(fld), str(NoneQuery(fld))) self.assertEquals("NoneQuery('u{}', True)".format(fld), From ac1a3851faa04fb7bca41bc5cbbdf403c42c0cc9 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 19:36:38 +0100 Subject: [PATCH 082/293] typo fix --- test/test_query.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_query.py b/test/test_query.py index 06f2deec2..fc8e76735 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -790,11 +790,11 @@ class NoneQueryTest(unittest.TestCase, TestHelper): def test_query_repr(self): fld = u'rg_track_gain' if sys.version_info <= (3, 0): - self.assertEquals("NoneQuery('u{}', True)".format(fld), + self.assertEquals("NoneQuery(u'{}', True)".format(fld), str(NoneQuery(fld))) - self.assertEquals("NoneQuery('u{}', True)".format(fld), + self.assertEquals("NoneQuery(u'{}', True)".format(fld), str(NoneQuery(fld, fast=True))) - self.assertEquals("NoneQuery('u{}', False)".format(fld), + self.assertEquals("NoneQuery(u'{}', False)".format(fld), str(NoneQuery(fld, fast=False))) else: self.assertEquals("NoneQuery('{}', True)".format(fld), From 4c6993989caf9e01d269ae03715adb6a175b23fd Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Sun, 15 Mar 2020 20:57:47 +0100 Subject: [PATCH 083/293] changelog entry --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index d3379b36c..457275763 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -171,6 +171,8 @@ Fixes: properly returns integer values, which should avoid problems where fields like ``bpm`` would sometimes store non-integer values. :bug:`762` :bug:`3507` :bug:`3508` +* Removed `@classmethod`` decorator from dbcore.query.NoneQuery.match method + failing with AttributeError when called. It is now an instance method.` For plugin developers: From 4ba7b8da313a535da8ad9972cc984557c04a5909 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 16 Mar 2020 10:12:09 +0100 Subject: [PATCH 084/293] sampsyo's docs/changelog.rst correction Co-Authored-By: Adrian Sampson --- docs/changelog.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 457275763..a51c85cad 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -171,8 +171,9 @@ Fixes: properly returns integer values, which should avoid problems where fields like ``bpm`` would sometimes store non-integer values. :bug:`762` :bug:`3507` :bug:`3508` -* Removed `@classmethod`` decorator from dbcore.query.NoneQuery.match method - failing with AttributeError when called. It is now an instance method.` +* Removed ``@classmethod`` decorator from dbcore.query.NoneQuery.match method + failing with AttributeError when called. It is now an instance method. + :bug:`3516` :bug:`3517` For plugin developers: From 611659d03c6e75b25aea994cf9daa0732df61679 Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 16 Mar 2020 10:16:18 +0100 Subject: [PATCH 085/293] removed value_match and repr tests --- beets/dbcore/query.py | 7 +------ test/test_query.py | 17 ----------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index e4f5b8a29..db4b861c4 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -152,17 +152,12 @@ class NoneQuery(FieldQuery): def __init__(self, field, fast=True): super(NoneQuery, self).__init__(field, None, fast) - self.pattern = None def col_clause(self): return self.field + " IS NULL", () def match(self, item): - return self.value_match(self.pattern, - item.get(self.field, default=None)) - - def value_match(self, pattern, value): - return value is pattern + return item.get(self.field, default=None) is None def __repr__(self): return "{0.__class__.__name__}({0.field!r}, {0.fast})".format(self) diff --git a/test/test_query.py b/test/test_query.py index fc8e76735..f88a12c92 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -787,23 +787,6 @@ class NoneQueryTest(unittest.TestCase, TestHelper): matched = self.lib.items(NoneQuery(u'rg_track_gain', fast=False)) self.assertInResult(item, matched) - def test_query_repr(self): - fld = u'rg_track_gain' - if sys.version_info <= (3, 0): - self.assertEquals("NoneQuery(u'{}', True)".format(fld), - str(NoneQuery(fld))) - self.assertEquals("NoneQuery(u'{}', True)".format(fld), - str(NoneQuery(fld, fast=True))) - self.assertEquals("NoneQuery(u'{}', False)".format(fld), - str(NoneQuery(fld, fast=False))) - else: - self.assertEquals("NoneQuery('{}', True)".format(fld), - str(NoneQuery(fld))) - self.assertEquals("NoneQuery('{}', True)".format(fld), - str(NoneQuery(fld, fast=True))) - self.assertEquals("NoneQuery('{}', False)".format(fld), - str(NoneQuery(fld, fast=False))) - class NotQueryMatchTest(_common.TestCase): """Test `query.NotQuery` matching against a single item, using the same From b34d1f71a93b3663f70f46a4d5ed08702b14c5ca Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 16 Mar 2020 09:08:57 -0400 Subject: [PATCH 086/293] Slightly terser `get` call for #3517 --- beets/dbcore/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index db4b861c4..4f19f4f8d 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -157,7 +157,7 @@ class NoneQuery(FieldQuery): return self.field + " IS NULL", () def match(self, item): - return item.get(self.field, default=None) is None + return item.get(self.field) is None def __repr__(self): return "{0.__class__.__name__}({0.field!r}, {0.fast})".format(self) From 19d6dfc8f301c4123989227084842d1c25807594 Mon Sep 17 00:00:00 2001 From: x1ppy <> Date: Thu, 23 Jan 2020 15:07:26 -0500 Subject: [PATCH 087/293] Support extra tags for MusicBrainz queries --- beets/autotag/hooks.py | 16 +++++++++++----- beets/autotag/match.py | 9 ++++++++- beets/autotag/mb.py | 22 ++++++++++++++++++++-- beets/config_default.yaml | 1 + beets/plugins.py | 7 ++++--- docs/changelog.rst | 2 ++ docs/reference/config.rst | 20 ++++++++++++++++++++ test/test_importer.py | 2 +- 8 files changed, 67 insertions(+), 12 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 030f371ba..d7c701db6 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -614,17 +614,21 @@ def tracks_for_id(track_id): @plugins.notify_info_yielded(u'albuminfo_received') -def album_candidates(items, artist, album, va_likely): +def album_candidates(items, artist, album, va_likely, extra_tags): """Search for album matches. ``items`` is a list of Item objects that make up the album. ``artist`` and ``album`` are the respective names (strings), which may be derived from the item list or may be entered by the user. ``va_likely`` is a boolean indicating whether - the album is likely to be a "various artists" release. + the album is likely to be a "various artists" release. ``extra_tags`` + is an optional dictionary of additional tags used to further + constrain the search. """ + # Base candidates if we have album and artist to match. if artist and album: try: - for candidate in mb.match_album(artist, album, len(items)): + for candidate in mb.match_album(artist, album, len(items), + extra_tags): yield candidate except mb.MusicBrainzAPIError as exc: exc.log(log) @@ -632,13 +636,15 @@ def album_candidates(items, artist, album, va_likely): # Also add VA matches from MusicBrainz where appropriate. if va_likely and album: try: - for candidate in mb.match_album(None, album, len(items)): + for candidate in mb.match_album(None, album, len(items), + extra_tags): yield candidate except mb.MusicBrainzAPIError as exc: exc.log(log) # Candidates from plugins. - for candidate in plugins.candidates(items, artist, album, va_likely): + for candidate in plugins.candidates(items, artist, album, va_likely, + extra_tags): yield candidate diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 71b62adb7..f57cac739 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -447,6 +447,12 @@ def tag_album(items, search_artist=None, search_album=None, search_artist, search_album = cur_artist, cur_album log.debug(u'Search terms: {0} - {1}', search_artist, search_album) + extra_tags = None + if config['musicbrainz']['extra_tags']: + tag_list = config['musicbrainz']['extra_tags'].get() + extra_tags = {k: v for (k, v) in likelies.items() if k in tag_list} + log.debug(u'Additional search terms: {0}', extra_tags) + # Is this album likely to be a "various artist" release? va_likely = ((not consensus['artist']) or (search_artist.lower() in VA_ARTISTS) or @@ -457,7 +463,8 @@ def tag_album(items, search_artist=None, search_album=None, for matched_candidate in hooks.album_candidates(items, search_artist, search_album, - va_likely): + va_likely, + extra_tags): _add_candidate(items, candidates, matched_candidate) log.debug(u'Evaluating {0} candidates.', len(candidates)) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 1a6e0b1f1..f86d3be71 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -38,6 +38,14 @@ else: SKIPPED_TRACKS = ['[data track]'] +FIELDS_TO_MB_KEYS = { + 'catalognum': 'catno', + 'country': 'country', + 'label': 'label', + 'media': 'format', + 'year': 'date', +} + musicbrainzngs.set_useragent('beets', beets.__version__, 'https://beets.io/') @@ -411,13 +419,13 @@ def album_info(release): return info -def match_album(artist, album, tracks=None): +def match_album(artist, album, tracks=None, extra_tags=None): """Searches for a single album ("release" in MusicBrainz parlance) and returns an iterator over AlbumInfo objects. May raise a MusicBrainzAPIError. The query consists of an artist name, an album name, and, - optionally, a number of tracks on the album. + optionally, a number of tracks on the album and any other extra tags. """ # Build search criteria. criteria = {'release': album.lower().strip()} @@ -429,6 +437,16 @@ def match_album(artist, album, tracks=None): if tracks is not None: criteria['tracks'] = six.text_type(tracks) + # Additional search cues from existing metadata. + if extra_tags: + for tag in extra_tags: + key = FIELDS_TO_MB_KEYS[tag] + value = six.text_type(extra_tags.get(tag, '')).lower().strip() + if key == 'catno': + value = value.replace(u' ', '') + if value: + criteria[key] = value + # Abort if we have no search terms. if not any(criteria.values()): return diff --git a/beets/config_default.yaml b/beets/config_default.yaml index cf9ae6bf9..0fd6eb592 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -103,6 +103,7 @@ musicbrainz: ratelimit: 1 ratelimit_interval: 1.0 searchlimit: 5 + extra_tags: [] match: strong_rec_thresh: 0.04 diff --git a/beets/plugins.py b/beets/plugins.py index 73d85cdd3..8606ebc69 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -172,7 +172,7 @@ class BeetsPlugin(object): """ return beets.autotag.hooks.Distance() - def candidates(self, items, artist, album, va_likely): + def candidates(self, items, artist, album, va_likely, extra_tags=None): """Should return a sequence of AlbumInfo objects that match the album whose items are provided. """ @@ -379,11 +379,12 @@ def album_distance(items, album_info, mapping): return dist -def candidates(items, artist, album, va_likely): +def candidates(items, artist, album, va_likely, extra_tags=None): """Gets MusicBrainz candidates for an album from each plugin. """ for plugin in find_plugins(): - for candidate in plugin.candidates(items, artist, album, va_likely): + for candidate in plugin.candidates(items, artist, album, va_likely, + extra_tags): yield candidate diff --git a/docs/changelog.rst b/docs/changelog.rst index a51c85cad..3769164be 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,8 @@ Changelog New features: +* A new :ref:`extra_tags` configuration option allows more tagged metadata + to be included in MusicBrainz queries. * A new :doc:`/plugins/fish` adds `Fish shell`_ tab autocompletion to beets * :doc:`plugins/fetchart` and :doc:`plugins/embedart`: Added a new ``quality`` option that controls the quality of the image output when the image is diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 7dcd53801..46f14f2c5 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -701,6 +701,26 @@ MusicBrainz server. Default: ``5``. +.. _extra_tags: + +extra_tags +~~~~~~~~~~ + +By default, beets will use only the artist, album, and track count to query +MusicBrainz. Additional tags to be queried can be supplied with the +``extra_tags`` setting. For example:: + + musicbrainz: + extra_tags: [year, catalognum, country, media, label] + +This setting should improve the autotagger results if the metadata with the +given tags match the metadata returned by MusicBrainz. + +Note that the only tags supported by this setting are the ones listed in the +above example. + +Default: ``[]`` + .. _match-config: Autotagger Matching Options diff --git a/test/test_importer.py b/test/test_importer.py index 8f637a077..3418d4628 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -79,7 +79,7 @@ class AutotagStub(object): autotag.mb.album_for_id = self.mb_album_for_id autotag.mb.track_for_id = self.mb_track_for_id - def match_album(self, albumartist, album, tracks): + def match_album(self, albumartist, album, tracks, extra_tags): if self.matching == self.IDENT: yield self._make_album_match(albumartist, album, tracks) From f306591a99f0a153d3116f36aaffa0ed5e5d4788 Mon Sep 17 00:00:00 2001 From: Andrew Rogl Date: Sat, 28 Mar 2020 17:36:02 +1000 Subject: [PATCH 088/293] add the extra_tags option to all required plugins --- beetsplug/beatport.py | 2 +- beetsplug/chroma.py | 2 +- beetsplug/cue.py | 2 +- beetsplug/discogs.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index 6a45ab93a..df0abb2fc 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -355,7 +355,7 @@ class BeatportPlugin(BeetsPlugin): config=self.config ) - def candidates(self, items, artist, release, va_likely): + def candidates(self, items, artist, release, va_likely, extra_tags=None): """Returns a list of AlbumInfo objects for beatport search results matching release and artist (if not various). """ diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index c4230b069..54ae90098 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -191,7 +191,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): dist.add_expr('track_id', info.track_id not in recording_ids) return dist - def candidates(self, items, artist, album, va_likely): + def candidates(self, items, artist, album, va_likely, extra_tags=None): albums = [] for relid in prefix(_all_releases(items), MAX_RELEASES): album = hooks.album_for_mbid(relid) diff --git a/beetsplug/cue.py b/beetsplug/cue.py index fd564b55c..92ca8784a 100644 --- a/beetsplug/cue.py +++ b/beetsplug/cue.py @@ -24,7 +24,7 @@ class CuePlugin(BeetsPlugin): # self.register_listener('import_task_start', self.look_for_cues) - def candidates(self, items, artist, album, va_likely): + def candidates(self, items, artist, album, va_likely, extra_tags=None): import pdb pdb.set_trace() diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 0ba27d7dd..86ace9aa8 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -175,7 +175,7 @@ class DiscogsPlugin(BeetsPlugin): config=self.config ) - def candidates(self, items, artist, album, va_likely): + def candidates(self, items, artist, album, va_likely, extra_tags=None): """Returns a list of AlbumInfo objects for discogs search results matching an album and artist (if not various). """ From 333d5d1dd3a1e578427328a3ce5a09ee342fcd4c Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Mon, 30 Mar 2020 20:08:40 +0100 Subject: [PATCH 089/293] fetchart: Add Last.fm artwork source --- beetsplug/fetchart.py | 70 ++++++++++++++++++++++++++++++++++++++- docs/changelog.rst | 3 ++ docs/plugins/fetchart.rst | 17 ++++++++-- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 2fe8b0b2c..28067c310 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -21,6 +21,7 @@ from contextlib import closing import os import re from tempfile import NamedTemporaryFile +from collections import OrderedDict import requests @@ -742,11 +743,72 @@ class FileSystem(LocalArtSource): match=Candidate.MATCH_FALLBACK) +class LastFM(RemoteArtSource): + NAME = u"Last.fm" + + # Sizes in priority order. + SIZES = OrderedDict([ + ('mega', (300, 300)), + ('extralarge', (300, 300)), + ('large', (174, 174)), + ('medium', (64, 64)), + ('small', (34, 34)), + ]) + + if util.SNI_SUPPORTED: + API_URL = 'https://ws.audioscrobbler.com/2.0' + else: + API_URL = 'http://ws.audioscrobbler.com/2.0' + + def __init__(self, *args, **kwargs): + super(LastFM, self).__init__(*args, **kwargs) + self.key = self._config['lastfm_key'].get(), + + def get(self, album, plugin, paths): + if not album.mb_albumid: + return + + try: + response = self.request(self.API_URL, params={ + 'method': 'album.getinfo', + 'api_key': self.key, + 'mbid': album.mb_albumid, + 'format': 'json', + }) + except requests.RequestException: + self._log.debug(u'lastfm: error receiving response') + return + + try: + data = response.json() + + if 'error' in data: + if data['error'] == 6: + self._log.debug('lastfm: no results for {}', + album.mb_albumid) + else: + self._log.error( + 'lastfm: failed to get album info: {} ({})', + data['message'], data['error']) + else: + images = {image['size']: image['#text'] + for image in data['album']['image']} + + # Provide candidates in order of size. + for size in self.SIZES.keys(): + if size in images: + yield self._candidate(url=images[size], + size=self.SIZES[size]) + except ValueError: + self._log.debug(u'lastfm: error loading response: {}' + .format(response.text)) + return + # Try each source in turn. SOURCES_ALL = [u'filesystem', u'coverart', u'itunes', u'amazon', u'albumart', - u'wikipedia', u'google', u'fanarttv'] + u'wikipedia', u'google', u'fanarttv', u'lastfm'] ART_SOURCES = { u'filesystem': FileSystem, @@ -757,6 +819,7 @@ ART_SOURCES = { u'wikipedia': Wikipedia, u'google': GoogleImages, u'fanarttv': FanartTV, + u'lastfm': LastFM, } SOURCE_NAMES = {v: k for k, v in ART_SOURCES.items()} @@ -787,11 +850,13 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): 'google_key': None, 'google_engine': u'001442825323518660753:hrh5ch1gjzm', 'fanarttv_key': None, + 'lastfm_key': None, 'store_source': False, 'high_resolution': False, }) self.config['google_key'].redact = True self.config['fanarttv_key'].redact = True + self.config['lastfm_key'].redact = True self.minwidth = self.config['minwidth'].get(int) self.maxwidth = self.config['maxwidth'].get(int) @@ -831,6 +896,9 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if not self.config['google_key'].get() and \ u'google' in available_sources: available_sources.remove(u'google') + if not self.config['lastfm_key'].get() and \ + u'lastfm' in available_sources: + available_sources.remove(u'lastfm') available_sources = [(s, c) for s in available_sources for c in ART_SOURCES[s].VALID_MATCHING_CRITERIA] diff --git a/docs/changelog.rst b/docs/changelog.rst index 3769164be..33ad386ad 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -113,6 +113,8 @@ New features: titles. Thanks to :user:`cole-miller`. :bug:`3459` +* :doc:`/plugins/fetchart`: Album art can now be fetched from `last.fm`_. + :bug:`3530` Fixes: @@ -233,6 +235,7 @@ For packagers: .. _works: https://musicbrainz.org/doc/Work .. _Deezer: https://www.deezer.com .. _keyfinder-cli: https://github.com/EvanPurkhiser/keyfinder-cli +.. _last.fm: https://last.fm 1.4.9 (May 30, 2019) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 4441d4e30..e8f7b6d92 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -58,9 +58,9 @@ file. The available options are: - **sources**: List of sources to search for images. An asterisk `*` expands to all available sources. Default: ``filesystem coverart itunes amazon albumart``, i.e., everything but - ``wikipedia``, ``google`` and ``fanarttv``. Enable those sources for more - matches at the cost of some speed. They are searched in the given order, - thus in the default config, no remote (Web) art source are queried if + ``wikipedia``, ``google``, ``fanarttv`` and ``lastfm``. Enable those sources + for more matches at the cost of some speed. They are searched in the given + order, thus in the default config, no remote (Web) art source are queried if local art is found in the filesystem. To use a local image as fallback, move it to the end of the list. For even more fine-grained control over the search order, see the section on :ref:`album-art-sources` below. @@ -71,6 +71,8 @@ file. The available options are: Default: The `beets custom search engine`_, which searches the entire web. - **fanarttv_key**: The personal API key for requesting art from fanart.tv. See below. +- **lastfm_key**: The personal API key for requesting art from Last.fm. See + below. - **store_source**: If enabled, fetchart stores the artwork's source in a flexible tag named ``art_source``. See below for the rationale behind this. Default: ``no``. @@ -221,6 +223,15 @@ personal key will give you earlier access to new art. .. _on their blog: https://fanart.tv/2015/01/personal-api-keys/ +Last.fm +''''''' + +To use the Last.fm backend, you need to `register for a Last.fm API key`_. Set +the ``lastfm_key`` configuration option to your API key, then add ``lastfm`` to +the list of sources in your configutation. + +.. _register for a Last.fm API key: https://www.last.fm/api/account/create + Storing the Artwork's Source ---------------------------- From 1bec3c4c9f6f29289c1b0f3709289cc2cdccc6ad Mon Sep 17 00:00:00 2001 From: Adam Jakab Date: Mon, 30 Mar 2020 22:15:13 +0200 Subject: [PATCH 090/293] added all my plugins + corrected 1 typo --- docs/plugins/index.rst | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 6c643ce61..76ed19fa3 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -300,12 +300,20 @@ Here are a few of the plugins written by the beets community: * `beet-summarize`_ can compute lots of counts and statistics about your music library. -* `beets-mosaic`_ generates a montage of a mosiac from cover art. +* `beets-mosaic`_ generates a montage of a mosaic from cover art. + +* `beets-goingrunning`_ generates playlists to go with your running sessions. + +* `beets-xtractor`_ extracts low- and high-level musical information from your songs. + +* `beets-yearfixer`_ attempts to fix all missing ``original_year`` and ``year`` fields. + +* `beets-autofix`_ automates repetitive tasks to keep your library in order. + +* `beets-describe`_ gives you the full picture of a single attribute of your library items. * `beets-bpmanalyser`_ analyses songs and calculates their tempo (BPM). -* `beets-goingrunning`_ copies songs to external device to go with your running session. - .. _beets-barcode: https://github.com/8h2a/beets-barcode .. _beets-check: https://github.com/geigerzaehler/beets-check .. _copyartifacts: https://github.com/sbarakat/beets-copyartifacts @@ -328,5 +336,9 @@ Here are a few of the plugins written by the beets community: .. _beets-ydl: https://github.com/vmassuchetto/beets-ydl .. _beet-summarize: https://github.com/steven-murray/beet-summarize .. _beets-mosaic: https://github.com/SusannaMaria/beets-mosaic +.. _beets-goingrunning: https://pypi.org/project/beets-goingrunning +.. _beets-xtractor: https://github.com/adamjakab/BeetsPluginXtractor +.. _beets-yearfixer: https://github.com/adamjakab/BeetsPluginYearFixer +.. _beets-autofix: https://github.com/adamjakab/BeetsPluginAutofix +.. _beets-describe: https://github.com/adamjakab/BeetsPluginDescribe .. _beets-bpmanalyser: https://github.com/adamjakab/BeetsPluginBpmAnalyser -.. _beets-goingrunning: https://pypi.org/project/beets-goingrunning/ From 8908416f6f66d2596a08daefeba0144cb27a7b98 Mon Sep 17 00:00:00 2001 From: x1ppy <59340737+x1ppy@users.noreply.github.com> Date: Mon, 6 Apr 2020 17:00:17 -0400 Subject: [PATCH 091/293] Add beets-originquery plugin --- docs/plugins/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 76ed19fa3..1247d0ed0 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -314,6 +314,9 @@ Here are a few of the plugins written by the beets community: * `beets-bpmanalyser`_ analyses songs and calculates their tempo (BPM). +* `beets-originquery`_ augments MusicBrainz queries with locally-sourced data + to improve autotagger results. + .. _beets-barcode: https://github.com/8h2a/beets-barcode .. _beets-check: https://github.com/geigerzaehler/beets-check .. _copyartifacts: https://github.com/sbarakat/beets-copyartifacts @@ -342,3 +345,4 @@ Here are a few of the plugins written by the beets community: .. _beets-autofix: https://github.com/adamjakab/BeetsPluginAutofix .. _beets-describe: https://github.com/adamjakab/BeetsPluginDescribe .. _beets-bpmanalyser: https://github.com/adamjakab/BeetsPluginBpmAnalyser +.. _beets-originquery: https://github.com/x1ppy/beets-originquery From 3c089117940de5d66504400db8b37603d98b60bf Mon Sep 17 00:00:00 2001 From: x1ppy <59340737+x1ppy@users.noreply.github.com> Date: Mon, 6 Apr 2020 17:01:11 -0400 Subject: [PATCH 092/293] Add missing extra_tags parameter to MetadataSourcePlugin (#3540) --- beets/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/plugins.py b/beets/plugins.py index 8606ebc69..695725cb8 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -715,7 +715,7 @@ class MetadataSourcePlugin(object): return id_ return None - def candidates(self, items, artist, album, va_likely): + def candidates(self, items, artist, album, va_likely, extra_tags=None): """Returns a list of AlbumInfo objects for Search API results matching an ``album`` and ``artist`` (if not various). From 8e28f0b694bc04fa0735c8d3f6feb8232647c8d7 Mon Sep 17 00:00:00 2001 From: lijacky Date: Sun, 12 Apr 2020 00:05:17 -0400 Subject: [PATCH 093/293] added null check for genius lyrics scrape --- beetsplug/lyrics.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 0e797d5a3..bb225007d 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -373,7 +373,13 @@ class Genius(Backend): # At least Genius is nice and has a tag called 'lyrics'! # Updated css where the lyrics are based in HTML. - lyrics = html.find("div", class_="lyrics").get_text() + try: + lyrics = html.find("div", class_="lyrics").get_text() + except AttributeError as exc: + # html is a NoneType cannot retrieve lyrics + self._log.debug(u'Genius lyrics for {0} not found: {1}', + page_url, exc) + return None return lyrics From 29d7b80847fd4f1d5c4f47d50d758160d6866c39 Mon Sep 17 00:00:00 2001 From: lijacky Date: Wed, 15 Apr 2020 22:23:27 -0400 Subject: [PATCH 094/293] Added unit test for null check --- test/test_lyrics.py | 61 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index f7ea538e2..e72b2d4f8 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -24,7 +24,7 @@ import sys import unittest from mock import patch -from test import _common +import _common from beets import logging from beets.library import Item @@ -39,6 +39,7 @@ from mock import MagicMock log = logging.getLogger('beets.test_lyrics') raw_backend = lyrics.Backend({}, log) google = lyrics.Google(MagicMock(), log) +genius = lyrics.Genius(MagicMock(), log) class LyricsPluginTest(unittest.TestCase): @@ -213,6 +214,29 @@ class MockFetchUrl(object): content = f.read() return content +class GeniusMockGet(object): + def __init__(self, pathval='fetched_path'): + self.pathval = pathval + self.fetched = None + + def __call__(self, url, headers=False): + from requests.models import Response + # for the first requests.get() return a path + if headers: + response = Response() + response.status_code = 200 + response._content = b'{"meta":{"status":200},"response":{"song":{"path":"/lyrics/sample"}}}' + return response + # for the second requests.get() return the genius page + else: + from mock import PropertyMock + self.fetched = url + fn = url_to_filename(url) + with open(fn, 'r') as f: + content = f.read() + response = Response() + type(response).text = PropertyMock(return_value=content) + return response def is_lyrics_content_ok(title, text): """Compare lyrics text to expected lyrics for given title.""" @@ -395,6 +419,41 @@ class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): google.is_page_candidate(url, url_title, s['title'], u'Sunn O)))') +class LyricsGeniusBaseTest(unittest.TestCase): + + def setUp(self): + """Set up configuration.""" + try: + __import__('bs4') + except ImportError: + self.skipTest('Beautiful Soup 4 not available') + if sys.version_info[:3] < (2, 7, 3): + self.skipTest("Python's built-in HTML parser is not good enough") + + +class LyricsGeniusScrapTest(LyricsGeniusBaseTest): + """Checks that Genius backend works as intended. + """ + import requests + def setUp(self): + """Set up configuration""" + LyricsGeniusBaseTest.setUp(self) + self.plugin = lyrics.LyricsPlugin() + + @patch.object(requests, 'get', GeniusMockGet()) + def test_no_lyrics_div(self): + """Ensure that `lyrics_from_song_api_path` doesn't crash when the html + for a Genius page contain
...
+ """ + # https://github.com/beetbox/beets/issues/3535 + # expected return value None + try: + self.assertEqual(genius.lyrics_from_song_api_path('/no_lyric_page'), None) + except AttributeError: + # if AttributeError we aren't doing a null check + self.assertTrue(False) + + class SlugTests(unittest.TestCase): def test_slug(self): From 525202e52919e971781a867f20d49b1548161d43 Mon Sep 17 00:00:00 2001 From: lijacky Date: Wed, 15 Apr 2020 22:30:31 -0400 Subject: [PATCH 095/293] adding genius sample html --- test/rsrc/lyrics/geniuscom/sample.txt | 1119 +++++++++++++++++++++++++ 1 file changed, 1119 insertions(+) create mode 100644 test/rsrc/lyrics/geniuscom/sample.txt diff --git a/test/rsrc/lyrics/geniuscom/sample.txt b/test/rsrc/lyrics/geniuscom/sample.txt new file mode 100644 index 000000000..da8a4d0b6 --- /dev/null +++ b/test/rsrc/lyrics/geniuscom/sample.txt @@ -0,0 +1,1119 @@ + + + + + + + +SAMPLE – SONG Lyrics | Genius Lyrics + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {{:: 'cloud_flare_always_on_short_message' | i18n }} +
Check @genius for updates. We'll have things fixed soon. +
+
+
+ + +
+ GENIUS +
+ + + +
+ + + + + + + + + + + +
+ + +
+ + + +
+ + + + + + + + + + + + + + + + +
+ +
+ +
+
+
+
+
+ Https%3a%2f%2fimages +
+
+ +
+
+ +

SONG

+

+ SAMPLE +

+ +

+ + +

+

+ + + + +

+

+ + + + +

+ +
+
+
+
+
+
+
+ + +
+
+
+ +

SONG Lyrics

+ +
+
+ + +

[Verse 1]
+Fearin' not growin' up
+Keepin' me up at night
+Am I doin' enough?
+Feel like I'm wastin' time
+
+[Pre-Chorus]
+Promise to get a little
+Better as I get older
+And you're so patient
+And sick of waitin'
+Promise to do better
+Shoulda coulda
+Prolly wanna let me go
+But you can't, oh
+Right now I feel it pourin'
+I need a little bit
+Just a little bit
+Just a little bit
+Right now I feel it pourin'
+I need a little bit
+Just a little bit
+Just a little bit
+
+[Chorus]
+Please don't take it, don't take it personal
+Like I know you usually do
+Please don't take it, don't take it personal
+Like I know you usually do
+Please, please
+Don't take it personal
+Don't take it personal
+Darling, like I know you will, ooh

+
+[Verse 2]
+Forget to call your mama on the weekend
+You should put yourself in time out
+(Shame, shame on you)
+But lately you've been feelin' so good
+I forget my future, never pull out
+(Shame, shame on me)
+Baby the money'll make it easier for me
+To run and hide out somewhere
+(So far away)
+Hoppin' through poppy fields
+Dodgin' evil witches
+These houses keep droppin' everywhere

+
+[Pre-Chorus]
+Promise to get a little
+Better as I get older
+And you're so patient
+And sick of waitin'
+Promise to do better
+Shoulda coulda
+Prolly wanna let me go
+But you can't, oh
+Right now I feel it pourin'
+I need a little bit
+Just a little bit
+Just a little bit
+Right now it's really pourin'
+I need a little bit
+Just a little bit
+Just a little bit
+
+[Chorus]
+Please don't take it, don't take it personal
+Like I know you usually do
+Please don't take it, take it personal

+
+[Outro]
+Like winters fall on us, heavy
+Take it off me, all it off
+Winter, I can't stand this
+Snow is falling all on me

+ + + + +
+
+ +
+ +
+
More on Genius
+ +
+ +
+
+
+ +
+
+ +
+
+
+ + + +
+ +

+ About “SONG” +

+ + +
+
+

The fifth track on SAMPLE’s debut album, ALBUM is the most reminiscent of her previous ep’s with a pop-disco sound.

+ +

She reflects about her insecurities on not maturing as quickly as her partner is. SONG season is usually the last big event before graduation for a high schooler, and SAMPLE is too busy living in the moment that she forgets she has her future to look forward to.

+ +

+ +

This is also around the age when friends are growing apart and have decided where and what they’re doing with their lives, and a lot are trying to hold on to the people they’ve known, even though they’re growing up differently. The person SAMPLE is talking to in the song seems to take everything, even change, to heart.

+
+ +
+ +
+
+
    + +
  • +
    +

    What have the artists said about this song

    +
    + +
    +

    SAMPLE tweeted:

    + +

    Iss called SONG cus I ain’t have no friends in high school so I went to Miami w my mama for SONG instead . Lol

    +
    + + +
  • + + + +
+
+ + +
+
+

"SONG" Track Info

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + +
+ +
+
+ + 1.   + + + + # + + + + +
+
+ +
+
+ + 2.   + + + + # + + + + +
+
+ +
+
+ + 3.   + + + + # + + + + +
+
+ +
+
+ + 4.   + + + + # + + + + +
+
+ +
+
+ + 5.   + + + + SONG + + + + +
+
+ +
+
+ + 6.   + + + + # + + + + +
+
+ +
+
+ + 7.   + + + + # + + + + +
+
+ +
+
+ + 8.   + + + + # + + + + +
+
+ +
+
+ + 9.   + + + + # + + + + +
+
+ +
+
+ + 10.   + + + + # + + + + +
+
+ +
+
+ + 11.   + + + + # + + + + +
+
+ +
+
+ + 12.   + + + + # + + + + +
+
+ +
+
+ + 13.   + + + + # + + + + +
+
+ +
+
+ + 14.   + + + + # + + + + +
+
+ + + +
+ +
+
+ +
+
+
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + From b7ecf32f2876848611dcced77c0a2624198d35a9 Mon Sep 17 00:00:00 2001 From: lijacky Date: Thu, 16 Apr 2020 17:20:08 -0400 Subject: [PATCH 096/293] style changes --- test/test_lyrics.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index e72b2d4f8..6c23c627a 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -24,7 +24,7 @@ import sys import unittest from mock import patch -import _common +from test import _common from beets import logging from beets.library import Item @@ -214,7 +214,9 @@ class MockFetchUrl(object): content = f.read() return content + class GeniusMockGet(object): + def __init__(self, pathval='fetched_path'): self.pathval = pathval self.fetched = None @@ -225,7 +227,8 @@ class GeniusMockGet(object): if headers: response = Response() response.status_code = 200 - response._content = b'{"meta":{"status":200},"response":{"song":{"path":"/lyrics/sample"}}}' + response._content = b'{"meta":{"status":200},\ + "response":{"song":{"path":"/lyrics/sample"}}}' return response # for the second requests.get() return the genius page else: @@ -238,6 +241,7 @@ class GeniusMockGet(object): type(response).text = PropertyMock(return_value=content) return response + def is_lyrics_content_ok(title, text): """Compare lyrics text to expected lyrics for given title.""" if not text: @@ -432,9 +436,11 @@ class LyricsGeniusBaseTest(unittest.TestCase): class LyricsGeniusScrapTest(LyricsGeniusBaseTest): + """Checks that Genius backend works as intended. """ import requests + def setUp(self): """Set up configuration""" LyricsGeniusBaseTest.setUp(self) @@ -443,12 +449,13 @@ class LyricsGeniusScrapTest(LyricsGeniusBaseTest): @patch.object(requests, 'get', GeniusMockGet()) def test_no_lyrics_div(self): """Ensure that `lyrics_from_song_api_path` doesn't crash when the html - for a Genius page contain
...
+ for a Genius page contain
""" # https://github.com/beetbox/beets/issues/3535 # expected return value None try: - self.assertEqual(genius.lyrics_from_song_api_path('/no_lyric_page'), None) + self.assertEqual(genius.lyrics_from_song_api_path('/nolyric'), + None) except AttributeError: # if AttributeError we aren't doing a null check self.assertTrue(False) From 9ec0d725e52a06863ac733e2f560dbc2b08fc014 Mon Sep 17 00:00:00 2001 From: lijacky Date: Fri, 17 Apr 2020 17:14:21 -0400 Subject: [PATCH 097/293] Changes given feedback on https://github.com/beetbox/beets/pull/3554 and trimmed sample html --- beetsplug/lyrics.py | 13 +- test/rsrc/lyrics/geniuscom/sample.txt | 1289 +++++-------------------- test/test_lyrics.py | 8 +- 3 files changed, 229 insertions(+), 1081 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index bb225007d..00b8820f4 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -373,13 +373,14 @@ class Genius(Backend): # At least Genius is nice and has a tag called 'lyrics'! # Updated css where the lyrics are based in HTML. - try: - lyrics = html.find("div", class_="lyrics").get_text() - except AttributeError as exc: - # html is a NoneType cannot retrieve lyrics - self._log.debug(u'Genius lyrics for {0} not found: {1}', - page_url, exc) + lyrics_div = html.find("div", class_="lyrics") + + # nullcheck + if lyrics_div is None: + self._log.debug(u'Genius lyrics for {0} not found', + page_url) return None + lyrics = lyrics_div.get_text() return lyrics diff --git a/test/rsrc/lyrics/geniuscom/sample.txt b/test/rsrc/lyrics/geniuscom/sample.txt index da8a4d0b6..1648d070a 100644 --- a/test/rsrc/lyrics/geniuscom/sample.txt +++ b/test/rsrc/lyrics/geniuscom/sample.txt @@ -1,1119 +1,270 @@ - + + //]]> + -SAMPLE – SONG Lyrics | Genius Lyrics + SAMPLE – SONG Lyrics | g-example Lyrics - - + + - + - + + + - + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ga('create', "UA-10346621-1", 'auto', {'useAmpClientId': true}); + ga('set', 'dimension1', "false"); + ga('set', 'dimension2', "songs#show"); + ga('set', 'dimension3', "r-b"); + ga('set', 'dimension4', "true"); + ga('set', 'dimension5', 'false'); + ga('set', 'dimension6', "none"); + ga('send', 'pageview'); + + - - + - - +
+
+
- - - - -
- - {{:: 'cloud_flare_always_on_short_message' | i18n }} -
Check @genius for updates. We'll have things fixed soon. -
-
-
- - -
- GENIUS -
- - - -
- - - - - - - - - - - -
- - -
- - - -
- - - - - - - - - - - - - - - - -
- -
- -
-
-
-
-
- Https%3a%2f%2fimages -
-
- -
-
- -

SONG

-

- SAMPLE -

- -

- - -

-

- - - - -

-

- - - - -

- -
-
-
-
-
-
-
- - -
-
-
- -

SONG Lyrics

- -
-
- - -

[Verse 1]
-Fearin' not growin' up
-Keepin' me up at night
-Am I doin' enough?
-Feel like I'm wastin' time
-
-[Pre-Chorus]
-Promise to get a little
-Better as I get older
-And you're so patient
-And sick of waitin'
-Promise to do better
-Shoulda coulda
-Prolly wanna let me go
-But you can't, oh
-Right now I feel it pourin'
-I need a little bit
-Just a little bit
-Just a little bit
-Right now I feel it pourin'
-I need a little bit
-Just a little bit
-Just a little bit
-
-[Chorus]
-Please don't take it, don't take it personal
-Like I know you usually do
-Please don't take it, don't take it personal
-Like I know you usually do
-Please, please
-Don't take it personal
-Don't take it personal
-Darling, like I know you will, ooh

-
-[Verse 2]
-Forget to call your mama on the weekend
-You should put yourself in time out
-(Shame, shame on you)
-But lately you've been feelin' so good
-I forget my future, never pull out
-(Shame, shame on me)
-Baby the money'll make it easier for me
-To run and hide out somewhere
-(So far away)
-Hoppin' through poppy fields
-Dodgin' evil witches
-These houses keep droppin' everywhere

-
-[Pre-Chorus]
-Promise to get a little
-Better as I get older
-And you're so patient
-And sick of waitin'
-Promise to do better
-Shoulda coulda
-Prolly wanna let me go
-But you can't, oh
-Right now I feel it pourin'
-I need a little bit
-Just a little bit
-Just a little bit
-Right now it's really pourin'
-I need a little bit
-Just a little bit
-Just a little bit
-
-[Chorus]
-Please don't take it, don't take it personal
-Like I know you usually do
-Please don't take it, take it personal

-
-[Outro]
-Like winters fall on us, heavy
-Take it off me, all it off
-Winter, I can't stand this
-Snow is falling all on me

- - - - -
-
- -
-
-
More on Genius
- + + + + +
-
-
-
- -
-
-
+ + - +
+
+
+
+
+ # +
+
-
+
+
+

SONG

+

+ + SAMPLE + +

+

+ +

+

+ +

+
+
+ +
+
+
+ +
+
+
+

SONG Lyrics

+
+
+ !!!! MISSING LYRICS HERE !!! +
+
+
+
+
More on g-example
+
+
+
+
+
-

- About “SONG” -

- - -
-
-

The fifth track on SAMPLE’s debut album, ALBUM is the most reminiscent of her previous ep’s with a pop-disco sound.

+ - -
- -
-
-
    - -
  • -
    -

    What have the artists said about this song

    -
    - -
    -

    SAMPLE tweeted:

    - -

    Iss called SONG cus I ain’t have no friends in high school so I went to Miami w my mama for SONG instead . Lol

    -
    - - -
  • - - - -
-
- - -
-
-

"SONG" Track Info

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + -
-
- -
- -
-
- - 1.   - - - - # - - - - -
-
- -
-
- - 2.   - - - - # - - - - -
-
- -
-
- - 3.   - - - - # - - - - -
-
- -
-
- - 4.   - - - - # - - - - -
-
- -
-
- - 5.   - - - - SONG - - - - -
-
- -
-
- - 6.   - - - - # - - - - -
-
- -
-
- - 7.   - - - - # - - - - -
-
- -
-
- - 8.   - - - - # - - - - -
-
- -
-
- - 9.   - - - - # - - - - -
-
- -
-
- - 10.   - - - - # - - - - -
-
- -
-
- - 11.   - - - - # - - - - -
-
- -
-
- - 12.   - - - - # - - - - -
-
- -
-
- - 13.   - - - - # - - - - -
-
- -
-
- - 14.   - - - - # - - - - -
-
- - - -
- + + # + +
-
- -
-
-
- - - - - - - + +