From 0191794285422eb4173421c04065f704f14a2b34 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 10 Nov 2019 21:59:58 +0100 Subject: [PATCH 01/47] 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 02/47] 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 03/47] 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 04/47] 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 05/47] 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 6d69d01016adaa780d660ee3c16199ad2afb96f5 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Mon, 13 Jan 2020 15:42:55 +0100 Subject: [PATCH 06/47] 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 981d4dc829b91c8bbc3d1821a93a6c68cedc9e86 Mon Sep 17 00:00:00 2001 From: soergeld Date: Sat, 25 Apr 2020 19:50:59 +0200 Subject: [PATCH 07/47] First try adding new albuminfo and trackinfo class --- beets/autotag/hooks.py | 171 ++++++++++++----------------------------- 1 file changed, 51 insertions(+), 120 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index d7c701db6..2555ed69f 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -39,8 +39,51 @@ except AttributeError: # Classes used to represent candidate options. +class Map(dict): + """ + Example: + m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer']) + """ + def __init__(self, *args, **kwargs): + super(Map, self).__init__(*args, **kwargs) + for arg in args: + if isinstance(arg, dict): + for k, v in arg.iteritems(): + self[k] = v + + if kwargs: + for k, v in kwargs.iteritems(): + self[k] = v + + def __getattr__(self, attr): + return self.get(attr) + + def __setattr__(self, key, value): + self.__setitem__(key, value) + + def __setitem__(self, key, value): + super(Map, self).__setitem__(key, value) + self.__dict__.update({key: value}) + + def __delattr__(self, item): + self.__delitem__(item) + + def __delitem__(self, key): + super(Map, self).__delitem__(key) + del self.__dict__[key] + + def __getstate__(self): + return self.__dict__ + + def __setstate__(self,state): + for key in state: + self.__setattr__(key,state[key]) + def __hash__(self): + return hash(tuple(sorted(self.items()))) -class AlbumInfo(object): + + +class AlbumInfo(Map): """Describes a canonical release that may be used to match a release in the library. Consists of these data members: @@ -49,77 +92,18 @@ class AlbumInfo(object): - ``artist``: name of the release's primary artist - ``artist_id`` - ``tracks``: list of TrackInfo objects making up the release - - ``asin``: Amazon ASIN - - ``albumtype``: string describing the kind of release - - ``va``: boolean: whether the release has "various artists" - - ``year``: release year - - ``month``: release month - - ``day``: release day - - ``label``: music label responsible for the release - - ``mediums``: the number of discs in this release - - ``artist_sort``: name of the release's artist for sorting - - ``releasegroup_id``: MBID for the album's release group - - ``catalognum``: the label's catalog number for the release - - ``script``: character set used for metadata - - ``language``: human language of the metadata - - ``country``: the release country - - ``albumstatus``: MusicBrainz release status (Official, etc.) - - ``media``: delivery mechanism (Vinyl, etc.) - - ``albumdisambig``: MusicBrainz release disambiguation comment - - ``releasegroupdisambig``: MusicBrainz release group - disambiguation comment. - - ``artist_credit``: Release-specific artist name - - ``data_source``: The original data source (MusicBrainz, Discogs, etc.) - - ``data_url``: The data source release URL. ``mediums`` along with the fields up through ``tracks`` are required. The others are optional and may be None. """ - def __init__(self, album, album_id, artist, artist_id, tracks, asin=None, - albumtype=None, va=False, year=None, month=None, day=None, - label=None, mediums=None, artist_sort=None, - releasegroup_id=None, catalognum=None, script=None, - language=None, country=None, style=None, genre=None, - albumstatus=None, media=None, albumdisambig=None, - releasegroupdisambig=None, artist_credit=None, - original_year=None, original_month=None, - original_day=None, data_source=None, data_url=None, - discogs_albumid=None, discogs_labelid=None, - discogs_artistid=None): + def __init__(self, album, album_id, artist, artist_id, tracks, **kwargs): self.album = album self.album_id = album_id self.artist = artist self.artist_id = artist_id self.tracks = tracks - self.asin = asin - self.albumtype = albumtype - self.va = va - self.year = year - self.month = month - self.day = day - self.label = label - self.mediums = mediums - self.artist_sort = artist_sort - self.releasegroup_id = releasegroup_id - self.catalognum = catalognum - self.script = script - self.language = language - self.country = country - self.style = style - self.genre = genre - self.albumstatus = albumstatus - self.media = media - self.albumdisambig = albumdisambig - self.releasegroupdisambig = releasegroupdisambig - self.artist_credit = artist_credit - self.original_year = original_year - self.original_month = original_month - self.original_day = original_day - self.data_source = data_source - self.data_url = data_url - self.discogs_albumid = discogs_albumid - self.discogs_labelid = discogs_labelid - self.discogs_artistid = discogs_artistid + for arg in kwargs: + self.__setattr__(arg,kwargs[arg]) # Work around a bug in python-musicbrainz-ngs that causes some # strings to be bytes rather than Unicode. @@ -143,75 +127,22 @@ class AlbumInfo(object): track.decode(codec) -class TrackInfo(object): +class TrackInfo(Map): """Describes a canonical track present on a release. Appears as part of an AlbumInfo's ``tracks`` list. Consists of these data members: - ``title``: name of the track - ``track_id``: MusicBrainz ID; UUID fragment only - - ``release_track_id``: MusicBrainz ID respective to a track on a - particular release; UUID fragment only - - ``artist``: individual track artist name - - ``artist_id`` - - ``length``: float: duration of the track in seconds - - ``index``: position on the entire release - - ``media``: delivery mechanism (Vinyl, etc.) - - ``medium``: the disc number this track appears on in the album - - ``medium_index``: the track's position on the disc - - ``medium_total``: the number of tracks on the item's disc - - ``artist_sort``: name of the track artist for sorting - - ``disctitle``: name of the individual medium (subtitle) - - ``artist_credit``: Recording-specific artist name - - ``data_source``: The original data source (MusicBrainz, Discogs, etc.) - - ``data_url``: The data source release URL. - - ``lyricist``: individual track lyricist name - - ``composer``: individual track composer name - - ``composer_sort``: individual track composer sort name - - ``arranger`: individual track arranger name - - ``track_alt``: alternative track number (tape, vinyl, etc.) - - ``work`: individual track work title - - ``mb_workid`: individual track work id - - ``work_disambig`: individual track work diambiguation Only ``title`` and ``track_id`` are required. The rest of the fields may be None. The indices ``index``, ``medium``, and ``medium_index`` are all 1-based. """ - def __init__(self, title, track_id, release_track_id=None, artist=None, - artist_id=None, length=None, index=None, medium=None, - medium_index=None, medium_total=None, artist_sort=None, - disctitle=None, artist_credit=None, data_source=None, - data_url=None, media=None, lyricist=None, composer=None, - composer_sort=None, arranger=None, track_alt=None, - work=None, mb_workid=None, work_disambig=None, bpm=None, - initial_key=None, genre=None): + def __init__(self, title, track_id,**kwargs): self.title = title self.track_id = track_id - self.release_track_id = release_track_id - self.artist = artist - self.artist_id = artist_id - self.length = length - self.index = index - self.media = media - self.medium = medium - self.medium_index = medium_index - self.medium_total = medium_total - self.artist_sort = artist_sort - self.disctitle = disctitle - self.artist_credit = artist_credit - self.data_source = data_source - self.data_url = data_url - self.lyricist = lyricist - self.composer = composer - self.composer_sort = composer_sort - self.arranger = arranger - self.track_alt = track_alt - self.work = work - self.mb_workid = mb_workid - self.work_disambig = work_disambig - self.bpm = bpm - self.initial_key = initial_key - self.genre = genre + for arg in kwargs: + self.__setattr__(arg,kwargs[arg]) # As above, work around a bug in python-musicbrainz-ngs. def decode(self, codec='utf-8'): From da43ff9c188276548c30237e6d93c6c45106bebe Mon Sep 17 00:00:00 2001 From: soergeld Date: Sat, 25 Apr 2020 20:43:30 +0200 Subject: [PATCH 08/47] arrange __getattr__ to behave normally --- beets/autotag/hooks.py | 94 ++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 2555ed69f..cc8e6a68a 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -39,49 +39,53 @@ except AttributeError: # Classes used to represent candidate options. -class Map(dict): - """ - Example: - m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer']) - """ - def __init__(self, *args, **kwargs): - super(Map, self).__init__(*args, **kwargs) - for arg in args: - if isinstance(arg, dict): - for k, v in arg.iteritems(): - self[k] = v - - if kwargs: - for k, v in kwargs.iteritems(): - self[k] = v - - def __getattr__(self, attr): - return self.get(attr) - - def __setattr__(self, key, value): - self.__setitem__(key, value) - - def __setitem__(self, key, value): - super(Map, self).__setitem__(key, value) - self.__dict__.update({key: value}) - - def __delattr__(self, item): - self.__delitem__(item) - - def __delitem__(self, key): - super(Map, self).__delitem__(key) - del self.__dict__[key] - - def __getstate__(self): - return self.__dict__ - - def __setstate__(self,state): - for key in state: - self.__setattr__(key,state[key]) - def __hash__(self): - return hash(tuple(sorted(self.items()))) +class Map(dict): + """ + Example: + m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, + sports=['Soccer']) + """ + def __init__(self, *args, **kwargs): + super(Map, self).__init__(*args, **kwargs) + for arg in args: + if isinstance(arg, dict): + for k, v in arg.iteritems(): + self[k] = v + + if kwargs: + for k, v in kwargs.iteritems(): + self[k] = v + + def __getattr__(self, attr): + if attr in self: + return self.get(attr) + else: + raise AttributeError + + def __setattr__(self, key, value): + self.__setitem__(key, value) + + def __setitem__(self, key, value): + super(Map, self).__setitem__(key, value) + self.__dict__.update({key: value}) + + def __delattr__(self, item): + self.__delitem__(item) + + def __delitem__(self, key): + super(Map, self).__delitem__(key) + del self.__dict__[key] + + def __getstate__(self): + return self.__dict__ + + def __setstate__(self, state): + for key in state: + self.__setattr__(key, state[key]) + + def __hash__(self): + return hash(tuple(sorted(self.items()))) - class AlbumInfo(Map): """Describes a canonical release that may be used to match a release @@ -103,7 +107,7 @@ class AlbumInfo(Map): self.artist_id = artist_id self.tracks = tracks for arg in kwargs: - self.__setattr__(arg,kwargs[arg]) + self.__setattr__(arg, kwargs[arg]) # Work around a bug in python-musicbrainz-ngs that causes some # strings to be bytes rather than Unicode. @@ -138,11 +142,11 @@ class TrackInfo(Map): may be None. The indices ``index``, ``medium``, and ``medium_index`` are all 1-based. """ - def __init__(self, title, track_id,**kwargs): + def __init__(self, title, track_id, **kwargs): self.title = title self.track_id = track_id for arg in kwargs: - self.__setattr__(arg,kwargs[arg]) + self.__setattr__(arg, kwargs[arg]) # As above, work around a bug in python-musicbrainz-ngs. def decode(self, codec='utf-8'): From fea6ffc0386eed693fdeef27556635eef4d44bbd Mon Sep 17 00:00:00 2001 From: soergeld Date: Sat, 25 Apr 2020 22:24:24 +0200 Subject: [PATCH 09/47] arrange decode, set all attributes to be flexible --- beets/autotag/hooks.py | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index cc8e6a68a..dd9726b84 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -100,14 +100,9 @@ class AlbumInfo(Map): ``mediums`` along with the fields up through ``tracks`` are required. The others are optional and may be None. """ - def __init__(self, album, album_id, artist, artist_id, tracks, **kwargs): - self.album = album - self.album_id = album_id - self.artist = artist - self.artist_id = artist_id - self.tracks = tracks + def __init__(self, **kwargs): for arg in kwargs: - self.__setattr__(arg, kwargs[arg]) + self.__setattr__(arg, kwargs[arg]) # Work around a bug in python-musicbrainz-ngs that causes some # strings to be bytes rather than Unicode. @@ -116,19 +111,11 @@ class AlbumInfo(Map): """Ensure that all string attributes on this object, and the constituent `TrackInfo` objects, are decoded to Unicode. """ - for fld in ['album', 'artist', 'albumtype', 'label', 'artist_sort', - 'catalognum', 'script', 'language', 'country', 'style', - 'genre', 'albumstatus', 'albumdisambig', - 'releasegroupdisambig', 'artist_credit', - 'media', 'discogs_albumid', 'discogs_labelid', - 'discogs_artistid']: + for fld in self: value = getattr(self, fld) - if isinstance(value, bytes): - setattr(self, fld, value.decode(codec, 'ignore')) - - if self.tracks: - for track in self.tracks: - track.decode(codec) + if type(value) == str: + if isinstance(value, bytes): + setattr(self, fld, value.decode(codec, 'ignore')) class TrackInfo(Map): @@ -142,22 +129,20 @@ class TrackInfo(Map): may be None. The indices ``index``, ``medium``, and ``medium_index`` are all 1-based. """ - def __init__(self, title, track_id, **kwargs): - self.title = title - self.track_id = track_id + def __init__(self, **kwargs): for arg in kwargs: - self.__setattr__(arg, kwargs[arg]) + self.__setattr__(arg, kwargs[arg]) # As above, work around a bug in python-musicbrainz-ngs. def decode(self, codec='utf-8'): """Ensure that all string attributes on this object are decoded to Unicode. """ - for fld in ['title', 'artist', 'medium', 'artist_sort', 'disctitle', - 'artist_credit', 'media']: + for fld in self: value = getattr(self, fld) - if isinstance(value, bytes): - setattr(self, fld, value.decode(codec, 'ignore')) + if type(value) == str: + if isinstance(value, bytes): + setattr(self, fld, value.decode(codec, 'ignore')) # Candidate distance scoring. From 53ce6f8b3d7d62c4ca54966042530f6c6e7d7c45 Mon Sep 17 00:00:00 2001 From: soergeld Date: Sat, 25 Apr 2020 22:32:03 +0200 Subject: [PATCH 10/47] all attributes are flexible, so no positional arguments when initiating the class --- beets/autotag/mb.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index f86d3be71..ea8ef24da 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -193,8 +193,8 @@ def track_info(recording, index=None, medium=None, medium_index=None, the number of tracks on the medium. Each number is a 1-based index. """ info = beets.autotag.hooks.TrackInfo( - recording['title'], - recording['id'], + title=recording['title'], + track_id=recording['id'], index=index, medium=medium, medium_index=medium_index, @@ -341,11 +341,11 @@ def album_info(release): track_infos.append(ti) info = beets.autotag.hooks.AlbumInfo( - release['title'], - release['id'], - artist_name, - release['artist-credit'][0]['artist']['id'], - track_infos, + album=release['title'], + album_id=release['id'], + artist=artist_name, + artist_id=release['artist-credit'][0]['artist']['id'], + tracks=track_infos, mediums=len(release['medium-list']), artist_sort=artist_sort_name, artist_credit=artist_credit_name, From 1b2c8398b11ade93621583e06c0aea24b83c39b5 Mon Sep 17 00:00:00 2001 From: soergeld Date: Sat, 25 Apr 2020 23:13:08 +0200 Subject: [PATCH 11/47] cleaning up beets/autotag/__init__.py --- beets/autotag/__init__.py | 154 ++++++++++++++++++++++---------------- 1 file changed, 89 insertions(+), 65 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index f9e38413e..2dcf93c52 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -41,31 +41,10 @@ log = logging.getLogger('beets') def apply_item_metadata(item, track_info): """Set an item's metadata from its matched TrackInfo object. """ - item.artist = track_info.artist - item.artist_sort = track_info.artist_sort - item.artist_credit = track_info.artist_credit - item.title = track_info.title - item.mb_trackid = track_info.track_id - item.mb_releasetrackid = track_info.release_track_id - if track_info.artist_id: - item.mb_artistid = track_info.artist_id - if track_info.data_source: - item.data_source = track_info.data_source - - if track_info.lyricist is not None: - item.lyricist = track_info.lyricist - if track_info.composer is not None: - item.composer = track_info.composer - if track_info.composer_sort is not None: - item.composer_sort = track_info.composer_sort - if track_info.arranger is not None: - item.arranger = track_info.arranger - if track_info.work is not None: - item.work = track_info.work - if track_info.mb_workid is not None: - item.mb_workid = track_info.mb_workid - if track_info.work_disambig is not None: - item.work_disambig = track_info.work_disambig + print('zer' in track_info) + for attr in track_info: + print(attr in track_info) + item.__setattr__(attr, getattr(track_info, attr)) # At the moment, the other metadata is left intact (including album # and track number). Perhaps these should be emptied? @@ -78,25 +57,50 @@ def apply_metadata(album_info, mapping): for item, track_info in mapping.items(): # Artist or artist credit. if config['artist_credit']: - item.artist = (track_info.artist_credit or - track_info.artist or - album_info.artist_credit or - album_info.artist) - item.albumartist = (album_info.artist_credit or - album_info.artist) + + if 'artist_credit' in track_info: + item.artist = track_info.artist_credit + elif 'artist' in track_info: + item.artist = track_info.artist + elif 'artist_credit' in album_info: + item.artist = album_info.artist_credit + elif 'artist' in album_info: + item.artist = album_info.artist + + if 'artist_credit' in album_info: + item.albumartist = album_info.artist_credit + elif 'artist' in album_info: + item.albumartist = album_info.artist + else: - item.artist = (track_info.artist or album_info.artist) - item.albumartist = album_info.artist + if 'artist' in track_info: + item.artist = track_info.artist + elif 'artist' in album_info: + item.artist = album_info.artist + + if 'artist' in album_info: + item.albumartist = album_info.artist # Album. - item.album = album_info.album + if 'album' in album_info: + item.album = album_info.album # Artist sort and credit names. - item.artist_sort = track_info.artist_sort or album_info.artist_sort - item.artist_credit = (track_info.artist_credit or - album_info.artist_credit) - item.albumartist_sort = album_info.artist_sort - item.albumartist_credit = album_info.artist_credit + if 'artist_sort' in track_info: + item.artist_sort = track_info.artist_sort + elif 'artist_sort' in album_info: + item.artist_sort = album_info.artist_sort + + if 'artist_credit' in track_info: + item.artist_credit = track_info.artist_credit + elif 'artist_credit' in album_info: + item.artist_credit = album_info.artist_credit + + if 'albumartist_sort' in album_info: + item.albumartist_sort = album_info.artist_sort + + if 'albumartist_credit' in album_info: + item.albumartist_credit = album_info.artist_credit # Release date. for prefix in '', 'original_': @@ -106,7 +110,10 @@ def apply_metadata(album_info, mapping): for suffix in 'year', 'month', 'day': key = prefix + suffix - value = getattr(album_info, key) or 0 + if key in album_info: + value = getattr(album_info, key) + else: + value = 0 # If we don't even have a year, apply nothing. if suffix == 'year' and not value: @@ -122,40 +129,55 @@ def apply_metadata(album_info, mapping): item[suffix] = value # Title. - item.title = track_info.title + if 'title' in track_info: + item.title = track_info.title if config['per_disc_numbering']: # We want to let the track number be zero, but if the medium index # is not provided we need to fall back to the overall index. - if track_info.medium_index is not None: + if 'medium_index' in track_info: item.track = track_info.medium_index - else: + elif 'index' in track_info: item.track = track_info.index - item.tracktotal = track_info.medium_total or len(album_info.tracks) + if 'medium_total' in track_info: + item.tracktotal = track_info.medium_total + elif 'tracks' in album_info: + item.tracktotal = len(album_info.tracks) else: - item.track = track_info.index - item.tracktotal = len(album_info.tracks) + if 'index' in track_info: + item.track = track_info.index + if 'tracks' in album_info: + item.tracktotal = len(album_info.tracks) # Disc and disc count. - item.disc = track_info.medium - item.disctotal = album_info.mediums + if 'medium' in track_info: + item.disc = track_info.medium + if 'mediums' in album_info: + item.disctotal = album_info.mediums # MusicBrainz IDs. - item.mb_trackid = track_info.track_id - item.mb_releasetrackid = track_info.release_track_id - item.mb_albumid = album_info.album_id - if track_info.artist_id: + if 'track_id' in track_info: + item.mb_trackid = track_info.track_id + if 'release_track_id' in track_info: + item.mb_releasetrackid = track_info.release_track_id + if 'album_id' in album_info: + item.mb_albumid = album_info.album_id + if 'artist_id' in track_info: item.mb_artistid = track_info.artist_id - else: + elif 'artist_id' in album_info: item.mb_artistid = album_info.artist_id - item.mb_albumartistid = album_info.artist_id - item.mb_releasegroupid = album_info.releasegroup_id + if 'artist_id' in album_info: + item.mb_albumartistid = album_info.artist_id + if 'releasegroup_id' in album_info: + item.mb_releasegroupid = album_info.releasegroup_id # Compilation flag. - item.comp = album_info.va + if 'va' in album_info: + item.comp = album_info.va # Track alt. - item.track_alt = track_info.track_alt + if 'track_alt' in track_info: + item.track_alt = track_info.track_alt # Miscellaneous/nullable metadata. misc_fields = { @@ -197,14 +219,16 @@ def apply_metadata(album_info, mapping): # field is explicitly allowed to be overwritten for field in misc_fields['album']: clobber = field in config['overwrite_null']['album'].as_str_seq() - value = getattr(album_info, field) - if value is None and not clobber: - continue - item[field] = value + if field in album_info: + value = getattr(album_info, field) + if value is None and not clobber: + continue + item[field] = value for field in misc_fields['track']: clobber = field in config['overwrite_null']['track'].as_str_seq() - value = getattr(track_info, field) - if value is None and not clobber: - continue - item[field] = value + if field in track_info: + value = getattr(track_info, field) + if value is None and not clobber: + continue + item[field] = value From 62566ee61ddde9c6de8d6e527820fa32acaf579f Mon Sep 17 00:00:00 2001 From: soergeld Date: Sat, 25 Apr 2020 23:13:38 +0200 Subject: [PATCH 12/47] remove prints for testing --- beets/autotag/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 2dcf93c52..530dcaf6e 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -41,9 +41,7 @@ log = logging.getLogger('beets') def apply_item_metadata(item, track_info): """Set an item's metadata from its matched TrackInfo object. """ - print('zer' in track_info) for attr in track_info: - print(attr in track_info) item.__setattr__(attr, getattr(track_info, attr)) # At the moment, the other metadata is left intact (including album From f507f0463906adfaae0aec6f0c0acbe3b1cb2814 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 11:21:13 +0200 Subject: [PATCH 13/47] reintroduce default arguments, adapt all occurences of TrackInfo and AlbumInfo to the absence of positional arguments --- beets/autotag/hooks.py | 84 +++++++++++++++++++++++++++++++++++++++++- beetsplug/cue.py | 3 +- beetsplug/discogs.py | 7 ++-- test/test_autotag.py | 47 +++++++++++------------ test/test_ui.py | 6 ++- 5 files changed, 116 insertions(+), 31 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index dd9726b84..fcb4ba161 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -100,7 +100,51 @@ class AlbumInfo(Map): ``mediums`` along with the fields up through ``tracks`` are required. The others are optional and may be None. """ - def __init__(self, **kwargs): + def __init__(self, album=None, album_id=None, artist=None, artist_id=None, + tracks, asin=None, albumtype=None, va=False, year=None, + month=None, day=None, label=None, mediums=None, + artist_sort=None, releasegroup_id=None, catalognum=None, + script=None, language=None, country=None, style=None, + genre=None, albumstatus=None, media=None, albumdisambig=None, + releasegroupdisambig=None, artist_credit=None, + original_year=None, original_month=None, + original_day=None, data_source=None, data_url=None, + discogs_albumid=None, discogs_labelid=None, + discogs_artistid=None, **kwargs): + self.album = album + self.album_id = album_id + self.artist = artist + self.artist_id = artist_id + self.tracks = tracks + self.asin = asin + self.albumtype = albumtype + self.va = va + self.year = year + self.month = month + self.day = day + self.label = label + self.mediums = mediums + self.artist_sort = artist_sort + self.releasegroup_id = releasegroup_id + self.catalognum = catalognum + self.script = script + self.language = language + self.country = country + self.style = style + self.genre = genre + self.albumstatus = albumstatus + self.media = media + self.albumdisambig = albumdisambig + self.releasegroupdisambig = releasegroupdisambig + self.artist_credit = artist_credit + self.original_year = original_year + self.original_month = original_month + self.original_day = original_day + self.data_source = data_source + self.data_url = data_url + self.discogs_albumid = discogs_albumid + self.discogs_labelid = discogs_labelid + self.discogs_artistid = discogs_artistid for arg in kwargs: self.__setattr__(arg, kwargs[arg]) @@ -129,7 +173,43 @@ class TrackInfo(Map): may be None. The indices ``index``, ``medium``, and ``medium_index`` are all 1-based. """ - def __init__(self, **kwargs): + def __init__(self, title=None, track_id=None, release_track_id=None, + artist=None, artist_id=None, length=None, index=None, + medium=None, medium_index=None, medium_total=None, + artist_sort=None, disctitle=None, artist_credit=None, + data_source=None, data_url=None, media=None, lyricist=None, + composer=None, composer_sort=None, arranger=None, + performer=None, track_alt=None, work=None, mb_workid=None, + work_disambig=None, bpm=None, initial_key=None, genre=None, + **kwargs): + self.title = title + self.track_id = track_id + self.release_track_id = release_track_id + self.artist = artist + self.artist_id = artist_id + self.length = length + self.index = index + self.media = media + self.medium = medium + self.medium_index = medium_index + self.medium_total = medium_total + self.artist_sort = artist_sort + self.disctitle = disctitle + self.artist_credit = artist_credit + self.data_source = data_source + self.data_url = data_url + self.lyricist = lyricist + self.composer = composer + self.composer_sort = composer_sort + self.arranger = arranger + self.performer = performer + self.track_alt = track_alt + self.work = work + self.mb_workid = mb_workid + self.work_disambig = work_disambig + self.bpm = bpm + self.initial_key = initial_key + self.genre = genre for arg in kwargs: self.__setattr__(arg, kwargs[arg]) diff --git a/beetsplug/cue.py b/beetsplug/cue.py index 92ca8784a..1ff817b2b 100644 --- a/beetsplug/cue.py +++ b/beetsplug/cue.py @@ -53,5 +53,6 @@ class CuePlugin(BeetsPlugin): title = "dunno lol" track_id = "wtf" index = int(path.basename(t)[len("split-track"):-len(".wav")]) - yield TrackInfo(title, track_id, index=index, artist=artist) + yield TrackInfo(title=title, track_id=track_id, index=index, + artist=artist) # generate TrackInfo instances diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 86ace9aa8..a0a6ea654 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -356,7 +356,8 @@ class DiscogsPlugin(BeetsPlugin): # a master release, otherwise fetch the master release. original_year = self.get_master_year(master_id) if master_id else year - return AlbumInfo(album, album_id, artist, artist_id, tracks, asin=None, + return AlbumInfo(album=album, album_id=album_id, artist=artist, + artist_id=artist_id, tracks=tracks, asin=None, albumtype=albumtype, va=va, year=year, month=None, day=None, label=label, mediums=len(set(mediums)), artist_sort=None, releasegroup_id=master_id, @@ -567,8 +568,8 @@ class DiscogsPlugin(BeetsPlugin): track.get('artists', []) ) length = self.get_track_length(track['duration']) - return TrackInfo(title, track_id, artist=artist, artist_id=artist_id, - length=length, index=index, + return TrackInfo(title=title, track_id=track_id, artist=artist, + artist_id=artist_id, length=length, index=index, medium=medium, medium_index=medium_index, artist_sort=None, disctitle=None, artist_credit=None) diff --git a/test/test_autotag.py b/test/test_autotag.py index a11bc8fac..3b9ff3e67 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -106,9 +106,9 @@ def _make_item(title, track, artist=u'some artist'): def _make_trackinfo(): return [ - TrackInfo(u'one', None, artist=u'some artist', length=1, index=1), - TrackInfo(u'two', None, artist=u'some artist', length=1, index=2), - TrackInfo(u'three', None, artist=u'some artist', length=1, index=3), + TrackInfo(title=u'one', track_id=one, artist=u'some artist', length=1, index=1), + TrackInfo(title=u'two', track_id=None, artist=u'some artist', length=1, index=2), + TrackInfo(title=u'three', track_id=None, artist=u'some artist', length=1, index=3), ] @@ -503,9 +503,9 @@ class AssignmentTest(unittest.TestCase): items.append(self.item(u'three', 2)) items.append(self.item(u'two', 3)) trackinfo = [] - trackinfo.append(TrackInfo(u'one', None)) - trackinfo.append(TrackInfo(u'two', None)) - trackinfo.append(TrackInfo(u'three', None)) + trackinfo.append(TrackInfo(title=u'one', track_id=None)) + trackinfo.append(TrackInfo(title=u'two', track_id=None)) + trackinfo.append(TrackInfo(title=u'three', track_id=None)) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, []) @@ -522,9 +522,9 @@ class AssignmentTest(unittest.TestCase): items.append(self.item(u'three', 1)) items.append(self.item(u'two', 1)) trackinfo = [] - trackinfo.append(TrackInfo(u'one', None)) - trackinfo.append(TrackInfo(u'two', None)) - trackinfo.append(TrackInfo(u'three', None)) + trackinfo.append(TrackInfo(title=u'one', track_id=None)) + trackinfo.append(TrackInfo(title=u'two', track_id=None)) + trackinfo.append(TrackInfo(title=u'three', track_id=None)) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, []) @@ -540,9 +540,9 @@ class AssignmentTest(unittest.TestCase): items.append(self.item(u'one', 1)) items.append(self.item(u'three', 3)) trackinfo = [] - trackinfo.append(TrackInfo(u'one', None)) - trackinfo.append(TrackInfo(u'two', None)) - trackinfo.append(TrackInfo(u'three', None)) + trackinfo.append(TrackInfo(title=u'one', track_id=None)) + trackinfo.append(TrackInfo(title=u'two', track_id=None)) + trackinfo.append(TrackInfo(title=u'three', track_id=None)) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, []) @@ -558,8 +558,8 @@ class AssignmentTest(unittest.TestCase): items.append(self.item(u'two', 2)) items.append(self.item(u'three', 3)) trackinfo = [] - trackinfo.append(TrackInfo(u'one', None)) - trackinfo.append(TrackInfo(u'three', None)) + trackinfo.append(TrackInfo(title=u'one', track_id=None)) + trackinfo.append(TrackInfo(title=u'three', track_id=None)) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, [items[1]]) @@ -595,7 +595,8 @@ class AssignmentTest(unittest.TestCase): items.append(item(12, 186.45916150485752)) def info(index, title, length): - return TrackInfo(title, None, length=length, index=index) + return TrackInfo(title=title, track_id=None, length=length, + index=index) trackinfo = [] trackinfo.append(info(1, u'Alone', 238.893)) trackinfo.append(info(2, u'The Woman in You', 341.44)) @@ -638,8 +639,8 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): self.items.append(Item({})) trackinfo = [] trackinfo.append(TrackInfo( - u'oneNew', - u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', + title=u'oneNew', + track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', medium=1, medium_index=1, medium_total=1, @@ -648,8 +649,8 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): artist_sort='trackArtistSort', )) trackinfo.append(TrackInfo( - u'twoNew', - u'40130ed1-a27c-42fd-a328-1ebefb6caef4', + title=u'twoNew', + track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', medium=2, medium_index=1, index=2, @@ -828,15 +829,15 @@ class ApplyCompilationTest(_common.TestCase, ApplyTestUtil): self.items.append(Item({})) trackinfo = [] trackinfo.append(TrackInfo( - u'oneNew', - u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', + title=u'oneNew', + track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', artist=u'artistOneNew', artist_id=u'a05686fc-9db2-4c23-b99e-77f5db3e5282', index=1, )) trackinfo.append(TrackInfo( - u'twoNew', - u'40130ed1-a27c-42fd-a328-1ebefb6caef4', + title=u'twoNew', + track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', artist=u'artistTwoNew', artist_id=u'80b3cf5e-18fe-4c59-98c7-e5bb87210710', index=2, diff --git a/test/test_ui.py b/test/test_ui.py index 110e80782..c4d502c73 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -1051,8 +1051,10 @@ class ShowChangeTest(_common.TestCase): self.items[0].track = 1 self.items[0].path = b'/path/to/file.mp3' self.info = autotag.AlbumInfo( - u'the album', u'album id', u'the artist', u'artist id', [ - autotag.TrackInfo(u'the title', u'track id', index=1) + album=u'the album', album_id=u'album id', artist=u'the artist', + artist_id=u'artist id', tracks=[ + autotag.TrackInfo(title=u'the title', track_id=u'track id', + index=1) ] ) From 32d81d801bd7d9eea9a6887cdf443e0675ce6215 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 11:25:42 +0200 Subject: [PATCH 14/47] forgot a positional argument --- beets/autotag/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index fcb4ba161..0017f9dd6 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -101,7 +101,7 @@ class AlbumInfo(Map): The others are optional and may be None. """ def __init__(self, album=None, album_id=None, artist=None, artist_id=None, - tracks, asin=None, albumtype=None, va=False, year=None, + tracks=None, asin=None, albumtype=None, va=False, year=None, month=None, day=None, label=None, mediums=None, artist_sort=None, releasegroup_id=None, catalognum=None, script=None, language=None, country=None, style=None, From 805f4e801f36cf73be8735daf601647b45b55770 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 11:31:39 +0200 Subject: [PATCH 15/47] typo in tests --- test/test_autotag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_autotag.py b/test/test_autotag.py index 3b9ff3e67..b894a65f3 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -106,7 +106,7 @@ def _make_item(title, track, artist=u'some artist'): def _make_trackinfo(): return [ - TrackInfo(title=u'one', track_id=one, artist=u'some artist', length=1, index=1), + TrackInfo(title=u'one', track_id=None, artist=u'some artist', length=1, index=1), TrackInfo(title=u'two', track_id=None, artist=u'some artist', length=1, index=2), TrackInfo(title=u'three', track_id=None, artist=u'some artist', length=1, index=3), ] From bd543136d66037e13400793d518ee74593d7f5f5 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 11:37:19 +0200 Subject: [PATCH 16/47] scale back some changes in __init__.py and hooks.py --- beets/autotag/__init__.py | 155 +++++++++++++++++--------------------- beets/autotag/hooks.py | 10 ++- 2 files changed, 76 insertions(+), 89 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 530dcaf6e..05b341a2d 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -41,8 +41,33 @@ log = logging.getLogger('beets') def apply_item_metadata(item, track_info): """Set an item's metadata from its matched TrackInfo object. """ - for attr in track_info: - item.__setattr__(attr, getattr(track_info, attr)) + item.artist = track_info.artist + item.artist_sort = track_info.artist_sort + item.artist_credit = track_info.artist_credit + item.title = track_info.title + item.mb_trackid = track_info.track_id + item.mb_releasetrackid = track_info.release_track_id + if track_info.artist_id: + item.mb_artistid = track_info.artist_id + if track_info.data_source: + item.data_source = track_info.data_source + + if track_info.lyricist is not None: + item.lyricist = track_info.lyricist + if track_info.composer is not None: + item.composer = track_info.composer + if track_info.composer_sort is not None: + item.composer_sort = track_info.composer_sort + if track_info.arranger is not None: + item.arranger = track_info.arranger + if track_info.performer is not None: + item.performer = track_info.performer + if track_info.work is not None: + item.work = track_info.work + if track_info.mb_workid is not None: + item.mb_workid = track_info.mb_workid + if track_info.work_disambig is not None: + item.work_disambig = track_info.work_disambig # At the moment, the other metadata is left intact (including album # and track number). Perhaps these should be emptied? @@ -55,50 +80,25 @@ def apply_metadata(album_info, mapping): for item, track_info in mapping.items(): # Artist or artist credit. if config['artist_credit']: - - if 'artist_credit' in track_info: - item.artist = track_info.artist_credit - elif 'artist' in track_info: - item.artist = track_info.artist - elif 'artist_credit' in album_info: - item.artist = album_info.artist_credit - elif 'artist' in album_info: - item.artist = album_info.artist - - if 'artist_credit' in album_info: - item.albumartist = album_info.artist_credit - elif 'artist' in album_info: - item.albumartist = album_info.artist - + item.artist = (track_info.artist_credit or + track_info.artist or + album_info.artist_credit or + album_info.artist) + item.albumartist = (album_info.artist_credit or + album_info.artist) else: - if 'artist' in track_info: - item.artist = track_info.artist - elif 'artist' in album_info: - item.artist = album_info.artist - - if 'artist' in album_info: - item.albumartist = album_info.artist + item.artist = (track_info.artist or album_info.artist) + item.albumartist = album_info.artist # Album. - if 'album' in album_info: - item.album = album_info.album + item.album = album_info.album # Artist sort and credit names. - if 'artist_sort' in track_info: - item.artist_sort = track_info.artist_sort - elif 'artist_sort' in album_info: - item.artist_sort = album_info.artist_sort - - if 'artist_credit' in track_info: - item.artist_credit = track_info.artist_credit - elif 'artist_credit' in album_info: - item.artist_credit = album_info.artist_credit - - if 'albumartist_sort' in album_info: - item.albumartist_sort = album_info.artist_sort - - if 'albumartist_credit' in album_info: - item.albumartist_credit = album_info.artist_credit + item.artist_sort = track_info.artist_sort or album_info.artist_sort + item.artist_credit = (track_info.artist_credit or + album_info.artist_credit) + item.albumartist_sort = album_info.artist_sort + item.albumartist_credit = album_info.artist_credit # Release date. for prefix in '', 'original_': @@ -108,10 +108,7 @@ def apply_metadata(album_info, mapping): for suffix in 'year', 'month', 'day': key = prefix + suffix - if key in album_info: - value = getattr(album_info, key) - else: - value = 0 + value = getattr(album_info, key) or 0 # If we don't even have a year, apply nothing. if suffix == 'year' and not value: @@ -127,55 +124,40 @@ def apply_metadata(album_info, mapping): item[suffix] = value # Title. - if 'title' in track_info: - item.title = track_info.title + item.title = track_info.title if config['per_disc_numbering']: # We want to let the track number be zero, but if the medium index # is not provided we need to fall back to the overall index. - if 'medium_index' in track_info: + if track_info.medium_index is not None: item.track = track_info.medium_index - elif 'index' in track_info: + else: item.track = track_info.index - if 'medium_total' in track_info: - item.tracktotal = track_info.medium_total - elif 'tracks' in album_info: - item.tracktotal = len(album_info.tracks) + item.tracktotal = track_info.medium_total or len(album_info.tracks) else: - if 'index' in track_info: - item.track = track_info.index - if 'tracks' in album_info: - item.tracktotal = len(album_info.tracks) + item.track = track_info.index + item.tracktotal = len(album_info.tracks) # Disc and disc count. - if 'medium' in track_info: - item.disc = track_info.medium - if 'mediums' in album_info: - item.disctotal = album_info.mediums + item.disc = track_info.medium + item.disctotal = album_info.mediums # MusicBrainz IDs. - if 'track_id' in track_info: - item.mb_trackid = track_info.track_id - if 'release_track_id' in track_info: - item.mb_releasetrackid = track_info.release_track_id - if 'album_id' in album_info: - item.mb_albumid = album_info.album_id - if 'artist_id' in track_info: + item.mb_trackid = track_info.track_id + item.mb_releasetrackid = track_info.release_track_id + item.mb_albumid = album_info.album_id + if track_info.artist_id: item.mb_artistid = track_info.artist_id - elif 'artist_id' in album_info: + else: item.mb_artistid = album_info.artist_id - if 'artist_id' in album_info: - item.mb_albumartistid = album_info.artist_id - if 'releasegroup_id' in album_info: - item.mb_releasegroupid = album_info.releasegroup_id + item.mb_albumartistid = album_info.artist_id + item.mb_releasegroupid = album_info.releasegroup_id # Compilation flag. - if 'va' in album_info: - item.comp = album_info.va + item.comp = album_info.va # Track alt. - if 'track_alt' in track_info: - item.track_alt = track_info.track_alt + item.track_alt = track_info.track_alt # Miscellaneous/nullable metadata. misc_fields = { @@ -204,6 +186,7 @@ def apply_metadata(album_info, mapping): 'composer', 'composer_sort', 'arranger', + 'performer', 'work', 'mb_workid', 'work_disambig', @@ -217,16 +200,14 @@ def apply_metadata(album_info, mapping): # field is explicitly allowed to be overwritten for field in misc_fields['album']: clobber = field in config['overwrite_null']['album'].as_str_seq() - if field in album_info: - value = getattr(album_info, field) - if value is None and not clobber: - continue - item[field] = value + value = getattr(album_info, field) + if value is None and not clobber: + continue + item[field] = value for field in misc_fields['track']: clobber = field in config['overwrite_null']['track'].as_str_seq() - if field in track_info: - value = getattr(track_info, field) - if value is None and not clobber: - continue - item[field] = value + value = getattr(track_info, field) + if value is None and not clobber: + continue + item[field] = value diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 0017f9dd6..1e856ca0a 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -155,7 +155,12 @@ class AlbumInfo(Map): """Ensure that all string attributes on this object, and the constituent `TrackInfo` objects, are decoded to Unicode. """ - for fld in self: + for fld in ['album', 'artist', 'albumtype', 'label', 'artist_sort', + 'catalognum', 'script', 'language', 'country', 'style', + 'genre', 'albumstatus', 'albumdisambig', + 'releasegroupdisambig', 'artist_credit', + 'media', 'discogs_albumid', 'discogs_labelid', + 'discogs_artistid']: value = getattr(self, fld) if type(value) == str: if isinstance(value, bytes): @@ -218,7 +223,8 @@ class TrackInfo(Map): """Ensure that all string attributes on this object are decoded to Unicode. """ - for fld in self: + for fld in ['title', 'artist', 'medium', 'artist_sort', 'disctitle', + 'artist_credit', 'media']: value = getattr(self, fld) if type(value) == str: if isinstance(value, bytes): From 363cbf8147940f471558e3545cc58ee2f491fb3e Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 11:39:20 +0200 Subject: [PATCH 17/47] mixed two PR --- beets/autotag/__init__.py | 3 --- beets/autotag/hooks.py | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 05b341a2d..f9e38413e 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -60,8 +60,6 @@ def apply_item_metadata(item, track_info): item.composer_sort = track_info.composer_sort if track_info.arranger is not None: item.arranger = track_info.arranger - if track_info.performer is not None: - item.performer = track_info.performer if track_info.work is not None: item.work = track_info.work if track_info.mb_workid is not None: @@ -186,7 +184,6 @@ def apply_metadata(album_info, mapping): 'composer', 'composer_sort', 'arranger', - 'performer', 'work', 'mb_workid', 'work_disambig', diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 1e856ca0a..48d1cae52 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -184,7 +184,7 @@ class TrackInfo(Map): artist_sort=None, disctitle=None, artist_credit=None, data_source=None, data_url=None, media=None, lyricist=None, composer=None, composer_sort=None, arranger=None, - performer=None, track_alt=None, work=None, mb_workid=None, + track_alt=None, work=None, mb_workid=None, work_disambig=None, bpm=None, initial_key=None, genre=None, **kwargs): self.title = title @@ -207,7 +207,6 @@ class TrackInfo(Map): self.composer = composer self.composer_sort = composer_sort self.arranger = arranger - self.performer = performer self.track_alt = track_alt self.work = work self.mb_workid = mb_workid From 14e1b33839afbf1c8fb1c4f94caf77bb99d82b96 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 11:44:51 +0200 Subject: [PATCH 18/47] lines too long --- test/test_autotag.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/test_autotag.py b/test/test_autotag.py index b894a65f3..fcf2ee5b1 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -106,9 +106,12 @@ def _make_item(title, track, artist=u'some artist'): def _make_trackinfo(): return [ - TrackInfo(title=u'one', track_id=None, artist=u'some artist', length=1, index=1), - TrackInfo(title=u'two', track_id=None, artist=u'some artist', length=1, index=2), - TrackInfo(title=u'three', track_id=None, artist=u'some artist', length=1, index=3), + TrackInfo(title=u'one', track_id=None, artist=u'some artist', + length=1, index=1), + TrackInfo(title=u'two', track_id=None, artist=u'some artist', + length=1, index=2), + TrackInfo(title=u'three', track_id=None, artist=u'some artist', + length=1, index=3), ] From 8df2e5b8f5affc9a5472886e819a31789c8fa280 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 12:01:35 +0200 Subject: [PATCH 19/47] arrange decoder --- beets/autotag/hooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 48d1cae52..95533d11d 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -162,7 +162,7 @@ class AlbumInfo(Map): 'media', 'discogs_albumid', 'discogs_labelid', 'discogs_artistid']: value = getattr(self, fld) - if type(value) == str: + if isinstance(value, bytes): if isinstance(value, bytes): setattr(self, fld, value.decode(codec, 'ignore')) @@ -225,7 +225,7 @@ class TrackInfo(Map): for fld in ['title', 'artist', 'medium', 'artist_sort', 'disctitle', 'artist_credit', 'media']: value = getattr(self, fld) - if type(value) == str: + if isinstance(value, bytes): if isinstance(value, bytes): setattr(self, fld, value.decode(codec, 'ignore')) From 98389b6161bcdad8cbbd8638eeedd01d877b25a3 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 12:02:27 +0200 Subject: [PATCH 20/47] dupe test --- beets/autotag/hooks.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 95533d11d..83abd8736 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -163,8 +163,7 @@ class AlbumInfo(Map): 'discogs_artistid']: value = getattr(self, fld) if isinstance(value, bytes): - if isinstance(value, bytes): - setattr(self, fld, value.decode(codec, 'ignore')) + setattr(self, fld, value.decode(codec, 'ignore')) class TrackInfo(Map): @@ -226,8 +225,7 @@ class TrackInfo(Map): 'artist_credit', 'media']: value = getattr(self, fld) if isinstance(value, bytes): - if isinstance(value, bytes): - setattr(self, fld, value.decode(codec, 'ignore')) + setattr(self, fld, value.decode(codec, 'ignore')) # Candidate distance scoring. From 63df7cf3edf9fc45fad4186d4332f30afe66a952 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 15:03:55 +0200 Subject: [PATCH 21/47] forgot to decode all tracks of an album --- beets/autotag/hooks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 83abd8736..a5d1ae428 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -165,6 +165,10 @@ class AlbumInfo(Map): if isinstance(value, bytes): setattr(self, fld, value.decode(codec, 'ignore')) + if 'tracks' in self: + for track in self.tracks: + track.decode(codec) + class TrackInfo(Map): """Describes a canonical track present on a release. Appears as part From 048b5c21517b88089723ecbca6282e337c976244 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 15:55:12 +0200 Subject: [PATCH 22/47] remove need for deepcopy, simplify __init__ --- beets/autotag/hooks.py | 18 +--- test/test_autotag.py | 199 +++++++++++++++++++++++++++++++++++++++-- test/test_ui.py | 7 +- 3 files changed, 202 insertions(+), 22 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index a5d1ae428..0c6e1eb80 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -45,13 +45,8 @@ class Map(dict): m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer']) """ - def __init__(self, *args, **kwargs): - super(Map, self).__init__(*args, **kwargs) - for arg in args: - if isinstance(arg, dict): - for k, v in arg.iteritems(): - self[k] = v - + def __init__(self, **kwargs): + super(Map, self).__init__(**kwargs) if kwargs: for k, v in kwargs.iteritems(): self[k] = v @@ -76,15 +71,8 @@ class Map(dict): super(Map, self).__delitem__(key) del self.__dict__[key] - def __getstate__(self): - return self.__dict__ - - def __setstate__(self, state): - for key in state: - self.__setattr__(key, state[key]) - def __hash__(self): - return hash(tuple(sorted(self.items()))) + return self.id class AlbumInfo(Map): diff --git a/test/test_autotag.py b/test/test_autotag.py index fcf2ee5b1..97030785b 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -753,13 +753,75 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): self.assertEqual(self.items[1].albumtype, 'album') def test_album_artist_overrides_empty_track_artist(self): - my_info = copy.deepcopy(self.info) + # make a deepcopy of self.info + trackinfo = [] + trackinfo.append(TrackInfo( + title=u'oneNew', + track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', + medium=1, + medium_index=1, + medium_total=1, + index=1, + artist_credit='trackArtistCredit', + artist_sort='trackArtistSort', + )) + trackinfo.append(TrackInfo( + title=u'twoNew', + track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', + medium=2, + medium_index=1, + index=2, + medium_total=1, + )) + my_info = AlbumInfo( + tracks=trackinfo, + artist=u'artistNew', + album=u'albumNew', + album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', + artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', + artist_credit=u'albumArtistCredit', + artist_sort=u'albumArtistSort', + albumtype=u'album', + va=False, + mediums=2, + ) self._apply(info=my_info) self.assertEqual(self.items[0].artist, 'artistNew') self.assertEqual(self.items[1].artist, 'artistNew') def test_album_artist_overridden_by_nonempty_track_artist(self): - my_info = copy.deepcopy(self.info) + # make a deepcopy of self.info + trackinfo = [] + trackinfo.append(TrackInfo( + title=u'oneNew', + track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', + medium=1, + medium_index=1, + medium_total=1, + index=1, + artist_credit='trackArtistCredit', + artist_sort='trackArtistSort', + )) + trackinfo.append(TrackInfo( + title=u'twoNew', + track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', + medium=2, + medium_index=1, + index=2, + medium_total=1, + )) + my_info = AlbumInfo( + tracks=trackinfo, + artist=u'artistNew', + album=u'albumNew', + album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', + artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', + artist_credit=u'albumArtistCredit', + artist_sort=u'albumArtistSort', + albumtype=u'album', + va=False, + mediums=2, + ) my_info.tracks[0].artist = 'artist1!' my_info.tracks[1].artist = 'artist2!' self._apply(info=my_info) @@ -781,7 +843,38 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): self.assertEqual(self.items[1].artist_sort, 'albumArtistSort') def test_full_date_applied(self): - my_info = copy.deepcopy(self.info) + # make a deepcopy of self.info + trackinfo = [] + trackinfo.append(TrackInfo( + title=u'oneNew', + track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', + medium=1, + medium_index=1, + medium_total=1, + index=1, + artist_credit='trackArtistCredit', + artist_sort='trackArtistSort', + )) + trackinfo.append(TrackInfo( + title=u'twoNew', + track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', + medium=2, + medium_index=1, + index=2, + medium_total=1, + )) + my_info = AlbumInfo( + tracks=trackinfo, + artist=u'artistNew', + album=u'albumNew', + album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', + artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', + artist_credit=u'albumArtistCredit', + artist_sort=u'albumArtistSort', + albumtype=u'album', + va=False, + mediums=2, + ) my_info.year = 2013 my_info.month = 12 my_info.day = 18 @@ -796,7 +889,38 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): self.items.append(Item(year=1, month=2, day=3)) self.items.append(Item(year=4, month=5, day=6)) - my_info = copy.deepcopy(self.info) + # make a deepcopy of self.info + trackinfo = [] + trackinfo.append(TrackInfo( + title=u'oneNew', + track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', + medium=1, + medium_index=1, + medium_total=1, + index=1, + artist_credit='trackArtistCredit', + artist_sort='trackArtistSort', + )) + trackinfo.append(TrackInfo( + title=u'twoNew', + track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', + medium=2, + medium_index=1, + index=2, + medium_total=1, + )) + my_info = AlbumInfo( + tracks=trackinfo, + artist=u'artistNew', + album=u'albumNew', + album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', + artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', + artist_credit=u'albumArtistCredit', + artist_sort=u'albumArtistSort', + albumtype=u'album', + va=False, + mediums=2, + ) my_info.year = 2013 self._apply(info=my_info) @@ -816,7 +940,38 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): self.assertEqual(self.items[0].day, 3) def test_data_source_applied(self): - my_info = copy.deepcopy(self.info) + # make a deepcopy of self.info + trackinfo = [] + trackinfo.append(TrackInfo( + title=u'oneNew', + track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', + medium=1, + medium_index=1, + medium_total=1, + index=1, + artist_credit='trackArtistCredit', + artist_sort='trackArtistSort', + )) + trackinfo.append(TrackInfo( + title=u'twoNew', + track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', + medium=2, + medium_index=1, + index=2, + medium_total=1, + )) + my_info = AlbumInfo( + tracks=trackinfo, + artist=u'artistNew', + album=u'albumNew', + album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', + artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', + artist_credit=u'albumArtistCredit', + artist_sort=u'albumArtistSort', + albumtype=u'album', + va=False, + mediums=2, + ) my_info.data_source = 'MusicBrainz' self._apply(info=my_info) @@ -827,6 +982,7 @@ class ApplyCompilationTest(_common.TestCase, ApplyTestUtil): def setUp(self): super(ApplyCompilationTest, self).setUp() + # make a deepcopy of self.info self.items = [] self.items.append(Item({})) self.items.append(Item({})) @@ -878,7 +1034,38 @@ class ApplyCompilationTest(_common.TestCase, ApplyTestUtil): self.assertFalse(self.items[1].comp) def test_va_flag_sets_comp(self): - va_info = copy.deepcopy(self.info) + # make a deepcopy of self.info + trackinfo = [] + trackinfo.append(TrackInfo( + title=u'oneNew', + track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', + medium=1, + medium_index=1, + medium_total=1, + index=1, + artist_credit='trackArtistCredit', + artist_sort='trackArtistSort', + )) + trackinfo.append(TrackInfo( + title=u'twoNew', + track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', + medium=2, + medium_index=1, + index=2, + medium_total=1, + )) + va_info = AlbumInfo( + tracks=trackinfo, + artist=u'artistNew', + album=u'albumNew', + album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', + artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', + artist_credit=u'albumArtistCredit', + artist_sort=u'albumArtistSort', + albumtype=u'album', + va=False, + mediums=2, + ) va_info.va = True self._apply(info=va_info) self.assertTrue(self.items[0].comp) diff --git a/test/test_ui.py b/test/test_ui.py index c4d502c73..608c1d7fb 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -1138,7 +1138,12 @@ class SummarizeItemsTest(_common.TestCase): summary = commands.summarize_items([self.item], False) self.assertEqual(summary, u"1 items, F, 4kbps, 10:54, 987.0 B") - i2 = deepcopy(self.item) + # make a copy of self.item + i2 = library.Item() + i2.bitrate = 4321 + i2.length = 10 * 60 + 54 + i2.format = "F" + summary = commands.summarize_items([self.item, i2], False) self.assertEqual(summary, u"2 items, F, 4kbps, 21:48, 1.9 KiB") From a51ef113d3fd1642f5c7298895697dae76fc9b1c Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 15:59:21 +0200 Subject: [PATCH 23/47] arranged hash --- beets/autotag/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 0c6e1eb80..350ca065c 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -72,7 +72,7 @@ class Map(dict): del self.__dict__[key] def __hash__(self): - return self.id + return id(self) class AlbumInfo(Map): From 2bc0027adf9824d7a312ae65ba0c045965eb4e60 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 16:08:47 +0200 Subject: [PATCH 24/47] remove some prints, unused libraries, __setitem__ method --- beets/autotag/hooks.py | 4 ---- test/test_autotag.py | 1 - test/test_ui.py | 1 - 3 files changed, 6 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 350ca065c..74145e78d 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -60,10 +60,6 @@ class Map(dict): def __setattr__(self, key, value): self.__setitem__(key, value) - def __setitem__(self, key, value): - super(Map, self).__setitem__(key, value) - self.__dict__.update({key: value}) - def __delattr__(self, item): self.__delitem__(item) diff --git a/test/test_autotag.py b/test/test_autotag.py index 97030785b..385b793ee 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -18,7 +18,6 @@ from __future__ import division, absolute_import, print_function import re -import copy import unittest from test import _common diff --git a/test/test_ui.py b/test/test_ui.py index 608c1d7fb..d0be10060 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -22,7 +22,6 @@ import shutil import re import subprocess import platform -from copy import deepcopy import six import unittest From eec7994dbe4cc3e12495de359989c1f6884f312c Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 16:16:37 +0200 Subject: [PATCH 25/47] corrected on deepcopy replacement --- test/test_autotag.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/test/test_autotag.py b/test/test_autotag.py index 385b793ee..af74b5c8f 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -1035,35 +1035,28 @@ class ApplyCompilationTest(_common.TestCase, ApplyTestUtil): def test_va_flag_sets_comp(self): # make a deepcopy of self.info trackinfo = [] + trackinfo = [] trackinfo.append(TrackInfo( title=u'oneNew', track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', - medium=1, - medium_index=1, - medium_total=1, + artist=u'artistOneNew', + artist_id=u'a05686fc-9db2-4c23-b99e-77f5db3e5282', index=1, - artist_credit='trackArtistCredit', - artist_sort='trackArtistSort', )) trackinfo.append(TrackInfo( title=u'twoNew', track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', - medium=2, - medium_index=1, + artist=u'artistTwoNew', + artist_id=u'80b3cf5e-18fe-4c59-98c7-e5bb87210710', index=2, - medium_total=1, )) va_info = AlbumInfo( tracks=trackinfo, - artist=u'artistNew', + artist=u'variousNew', album=u'albumNew', - album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', - artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', - artist_credit=u'albumArtistCredit', - artist_sort=u'albumArtistSort', - albumtype=u'album', - va=False, - mediums=2, + album_id='3b69ea40-39b8-487f-8818-04b6eff8c21a', + artist_id='89ad4ac3-39f7-470e-963a-56509c546377', + albumtype=u'compilation', ) va_info.va = True self._apply(info=va_info) From d07a3e36970aa785a8085afd56eaa4062be93787 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 16:17:11 +0200 Subject: [PATCH 26/47] remove __delitem__ and __delattr__ methods --- beets/autotag/hooks.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 74145e78d..36ea8db1e 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -60,13 +60,6 @@ class Map(dict): def __setattr__(self, key, value): self.__setitem__(key, value) - def __delattr__(self, item): - self.__delitem__(item) - - def __delitem__(self, key): - super(Map, self).__delitem__(key) - del self.__dict__[key] - def __hash__(self): return id(self) From 13db4063fa0d5515f8a479e90c1a2235c6e4cd06 Mon Sep 17 00:00:00 2001 From: soergeld Date: Mon, 27 Apr 2020 16:31:38 +0200 Subject: [PATCH 27/47] actually, I don't need __init__ --- beets/autotag/hooks.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 36ea8db1e..1500538d3 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -45,11 +45,6 @@ class Map(dict): m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer']) """ - def __init__(self, **kwargs): - super(Map, self).__init__(**kwargs) - if kwargs: - for k, v in kwargs.iteritems(): - self[k] = v def __getattr__(self, attr): if attr in self: From b39ef0b8f949ea41d0d9e40674b20d9201c370a3 Mon Sep 17 00:00:00 2001 From: soergeld Date: Tue, 28 Apr 2020 12:16:19 +0200 Subject: [PATCH 28/47] better deepcopy, docstring, minor improvements --- beets/autotag/hooks.py | 26 +++--- test/test_autotag.py | 198 +++++------------------------------------ 2 files changed, 40 insertions(+), 184 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 1500538d3..81c2fadb1 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -39,11 +39,19 @@ except AttributeError: # Classes used to represent candidate options. -class Map(dict): +class AttrDict(dict): """ - Example: - m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, - sports=['Soccer']) + Dictionary with flexible attributes + to get an tag value: + value = info.tag + or value = info[tag] + or value = info.get(tag) + or value = getattr(info, tag) + all raise AttributeError when info doesn't have tag + to set a tag value: + info.tag = value + or info[tag] = value + or setattr(info, tag, value) """ def __getattr__(self, attr): @@ -59,7 +67,7 @@ class Map(dict): return id(self) -class AlbumInfo(Map): +class AlbumInfo(AttrDict): """Describes a canonical release that may be used to match a release in the library. Consists of these data members: @@ -117,8 +125,7 @@ class AlbumInfo(Map): self.discogs_albumid = discogs_albumid self.discogs_labelid = discogs_labelid self.discogs_artistid = discogs_artistid - for arg in kwargs: - self.__setattr__(arg, kwargs[arg]) + self.update(kwargs) # Work around a bug in python-musicbrainz-ngs that causes some # strings to be bytes rather than Unicode. @@ -142,7 +149,7 @@ class AlbumInfo(Map): track.decode(codec) -class TrackInfo(Map): +class TrackInfo(AttrDict): """Describes a canonical track present on a release. Appears as part of an AlbumInfo's ``tracks`` list. Consists of these data members: @@ -189,8 +196,7 @@ class TrackInfo(Map): self.bpm = bpm self.initial_key = initial_key self.genre = genre - for arg in kwargs: - self.__setattr__(arg, kwargs[arg]) + self.update(kwargs) # As above, work around a bug in python-musicbrainz-ngs. def decode(self, codec='utf-8'): diff --git a/test/test_autotag.py b/test/test_autotag.py index af74b5c8f..3532a80ec 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -754,36 +754,10 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def test_album_artist_overrides_empty_track_artist(self): # make a deepcopy of self.info trackinfo = [] - trackinfo.append(TrackInfo( - title=u'oneNew', - track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', - medium=1, - medium_index=1, - medium_total=1, - index=1, - artist_credit='trackArtistCredit', - artist_sort='trackArtistSort', - )) - trackinfo.append(TrackInfo( - title=u'twoNew', - track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', - medium=2, - medium_index=1, - index=2, - medium_total=1, - )) - my_info = AlbumInfo( - tracks=trackinfo, - artist=u'artistNew', - album=u'albumNew', - album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', - artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', - artist_credit=u'albumArtistCredit', - artist_sort=u'albumArtistSort', - albumtype=u'album', - va=False, - mediums=2, - ) + trackinfo.append(TrackInfo(self.info.tracks[0])) + trackinfo.append(TrackInfo(self.info.tracks[1])) + my_info = AlbumInfo(self.info) + my_info.tracks = trackinfo self._apply(info=my_info) self.assertEqual(self.items[0].artist, 'artistNew') self.assertEqual(self.items[1].artist, 'artistNew') @@ -791,36 +765,10 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def test_album_artist_overridden_by_nonempty_track_artist(self): # make a deepcopy of self.info trackinfo = [] - trackinfo.append(TrackInfo( - title=u'oneNew', - track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', - medium=1, - medium_index=1, - medium_total=1, - index=1, - artist_credit='trackArtistCredit', - artist_sort='trackArtistSort', - )) - trackinfo.append(TrackInfo( - title=u'twoNew', - track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', - medium=2, - medium_index=1, - index=2, - medium_total=1, - )) - my_info = AlbumInfo( - tracks=trackinfo, - artist=u'artistNew', - album=u'albumNew', - album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', - artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', - artist_credit=u'albumArtistCredit', - artist_sort=u'albumArtistSort', - albumtype=u'album', - va=False, - mediums=2, - ) + trackinfo.append(TrackInfo(self.info.tracks[0])) + trackinfo.append(TrackInfo(self.info.tracks[1])) + my_info = AlbumInfo(self.info) + my_info.tracks = trackinfo my_info.tracks[0].artist = 'artist1!' my_info.tracks[1].artist = 'artist2!' self._apply(info=my_info) @@ -844,36 +792,10 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def test_full_date_applied(self): # make a deepcopy of self.info trackinfo = [] - trackinfo.append(TrackInfo( - title=u'oneNew', - track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', - medium=1, - medium_index=1, - medium_total=1, - index=1, - artist_credit='trackArtistCredit', - artist_sort='trackArtistSort', - )) - trackinfo.append(TrackInfo( - title=u'twoNew', - track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', - medium=2, - medium_index=1, - index=2, - medium_total=1, - )) - my_info = AlbumInfo( - tracks=trackinfo, - artist=u'artistNew', - album=u'albumNew', - album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', - artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', - artist_credit=u'albumArtistCredit', - artist_sort=u'albumArtistSort', - albumtype=u'album', - va=False, - mediums=2, - ) + trackinfo.append(TrackInfo(self.info.tracks[0])) + trackinfo.append(TrackInfo(self.info.tracks[1])) + my_info = AlbumInfo(self.info) + my_info.tracks = trackinfo my_info.year = 2013 my_info.month = 12 my_info.day = 18 @@ -890,36 +812,10 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): # make a deepcopy of self.info trackinfo = [] - trackinfo.append(TrackInfo( - title=u'oneNew', - track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', - medium=1, - medium_index=1, - medium_total=1, - index=1, - artist_credit='trackArtistCredit', - artist_sort='trackArtistSort', - )) - trackinfo.append(TrackInfo( - title=u'twoNew', - track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', - medium=2, - medium_index=1, - index=2, - medium_total=1, - )) - my_info = AlbumInfo( - tracks=trackinfo, - artist=u'artistNew', - album=u'albumNew', - album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', - artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', - artist_credit=u'albumArtistCredit', - artist_sort=u'albumArtistSort', - albumtype=u'album', - va=False, - mediums=2, - ) + trackinfo.append(TrackInfo(self.info.tracks[0])) + trackinfo.append(TrackInfo(self.info.tracks[1])) + my_info = AlbumInfo(self.info) + my_info.tracks = trackinfo my_info.year = 2013 self._apply(info=my_info) @@ -941,36 +837,10 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def test_data_source_applied(self): # make a deepcopy of self.info trackinfo = [] - trackinfo.append(TrackInfo( - title=u'oneNew', - track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', - medium=1, - medium_index=1, - medium_total=1, - index=1, - artist_credit='trackArtistCredit', - artist_sort='trackArtistSort', - )) - trackinfo.append(TrackInfo( - title=u'twoNew', - track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', - medium=2, - medium_index=1, - index=2, - medium_total=1, - )) - my_info = AlbumInfo( - tracks=trackinfo, - artist=u'artistNew', - album=u'albumNew', - album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7', - artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50', - artist_credit=u'albumArtistCredit', - artist_sort=u'albumArtistSort', - albumtype=u'album', - va=False, - mediums=2, - ) + trackinfo.append(TrackInfo(self.info.tracks[0])) + trackinfo.append(TrackInfo(self.info.tracks[1])) + my_info = AlbumInfo(self.info) + my_info.tracks = trackinfo my_info.data_source = 'MusicBrainz' self._apply(info=my_info) @@ -981,7 +851,6 @@ class ApplyCompilationTest(_common.TestCase, ApplyTestUtil): def setUp(self): super(ApplyCompilationTest, self).setUp() - # make a deepcopy of self.info self.items = [] self.items.append(Item({})) self.items.append(Item({})) @@ -1035,29 +904,10 @@ class ApplyCompilationTest(_common.TestCase, ApplyTestUtil): def test_va_flag_sets_comp(self): # make a deepcopy of self.info trackinfo = [] - trackinfo = [] - trackinfo.append(TrackInfo( - title=u'oneNew', - track_id=u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c', - artist=u'artistOneNew', - artist_id=u'a05686fc-9db2-4c23-b99e-77f5db3e5282', - index=1, - )) - trackinfo.append(TrackInfo( - title=u'twoNew', - track_id=u'40130ed1-a27c-42fd-a328-1ebefb6caef4', - artist=u'artistTwoNew', - artist_id=u'80b3cf5e-18fe-4c59-98c7-e5bb87210710', - index=2, - )) - va_info = AlbumInfo( - tracks=trackinfo, - artist=u'variousNew', - album=u'albumNew', - album_id='3b69ea40-39b8-487f-8818-04b6eff8c21a', - artist_id='89ad4ac3-39f7-470e-963a-56509c546377', - albumtype=u'compilation', - ) + trackinfo.append(TrackInfo(self.info.tracks[0])) + trackinfo.append(TrackInfo(self.info.tracks[1])) + va_info = AlbumInfo(self.info) + va_info.tracks = trackinfo va_info.va = True self._apply(info=va_info) self.assertTrue(self.items[0].comp) From ba2b22cac581d4cb304457b57fe5e1555bc18e45 Mon Sep 17 00:00:00 2001 From: soergeld Date: Tue, 28 Apr 2020 12:59:52 +0200 Subject: [PATCH 29/47] try to correct deepcopy --- test/test_autotag.py | 66 ++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/test/test_autotag.py b/test/test_autotag.py index 3532a80ec..06e6b7e72 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -754,9 +754,14 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def test_album_artist_overrides_empty_track_artist(self): # make a deepcopy of self.info trackinfo = [] - trackinfo.append(TrackInfo(self.info.tracks[0])) - trackinfo.append(TrackInfo(self.info.tracks[1])) - my_info = AlbumInfo(self.info) + track = TrackInfo() + track.update(self.info.tracks[0]) + trackinfo.append(track) + track = TrackInfo() + track.update(self.info.tracks[1]) + trackinfo.append(track) + my_info = AlbumInfo() + my_info.update(self.info) my_info.tracks = trackinfo self._apply(info=my_info) self.assertEqual(self.items[0].artist, 'artistNew') @@ -765,9 +770,14 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def test_album_artist_overridden_by_nonempty_track_artist(self): # make a deepcopy of self.info trackinfo = [] - trackinfo.append(TrackInfo(self.info.tracks[0])) - trackinfo.append(TrackInfo(self.info.tracks[1])) - my_info = AlbumInfo(self.info) + track = TrackInfo() + track.update(self.info.tracks[0]) + trackinfo.append(track) + track = TrackInfo() + track.update(self.info.tracks[1]) + trackinfo.append(track) + my_info = AlbumInfo() + my_info.update(self.info) my_info.tracks = trackinfo my_info.tracks[0].artist = 'artist1!' my_info.tracks[1].artist = 'artist2!' @@ -792,9 +802,14 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def test_full_date_applied(self): # make a deepcopy of self.info trackinfo = [] - trackinfo.append(TrackInfo(self.info.tracks[0])) - trackinfo.append(TrackInfo(self.info.tracks[1])) - my_info = AlbumInfo(self.info) + track = TrackInfo() + track.update(self.info.tracks[0]) + trackinfo.append(track) + track = TrackInfo() + track.update(self.info.tracks[1]) + trackinfo.append(track) + my_info = AlbumInfo() + my_info.update(self.info) my_info.tracks = trackinfo my_info.year = 2013 my_info.month = 12 @@ -812,9 +827,14 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): # make a deepcopy of self.info trackinfo = [] - trackinfo.append(TrackInfo(self.info.tracks[0])) - trackinfo.append(TrackInfo(self.info.tracks[1])) - my_info = AlbumInfo(self.info) + track = TrackInfo() + track.update(self.info.tracks[0]) + trackinfo.append(track) + track = TrackInfo() + track.update(self.info.tracks[1]) + trackinfo.append(track) + my_info = AlbumInfo() + my_info.update(self.info) my_info.tracks = trackinfo my_info.year = 2013 self._apply(info=my_info) @@ -837,9 +857,14 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def test_data_source_applied(self): # make a deepcopy of self.info trackinfo = [] - trackinfo.append(TrackInfo(self.info.tracks[0])) - trackinfo.append(TrackInfo(self.info.tracks[1])) - my_info = AlbumInfo(self.info) + track = TrackInfo() + track.update(self.info.tracks[0]) + trackinfo.append(track) + track = TrackInfo() + track.update(self.info.tracks[1]) + trackinfo.append(track) + my_info = AlbumInfo() + my_info.update(self.info) my_info.tracks = trackinfo my_info.data_source = 'MusicBrainz' self._apply(info=my_info) @@ -904,9 +929,14 @@ class ApplyCompilationTest(_common.TestCase, ApplyTestUtil): def test_va_flag_sets_comp(self): # make a deepcopy of self.info trackinfo = [] - trackinfo.append(TrackInfo(self.info.tracks[0])) - trackinfo.append(TrackInfo(self.info.tracks[1])) - va_info = AlbumInfo(self.info) + track = TrackInfo() + track.update(self.info.tracks[0]) + trackinfo.append(track) + track = TrackInfo() + track.update(self.info.tracks[1]) + trackinfo.append(track) + va_info = AlbumInfo() + va_info.update(self.info) va_info.tracks = trackinfo va_info.va = True self._apply(info=va_info) From 84bbd14c767d3a720e57b5572f9c8f443c18896a Mon Sep 17 00:00:00 2001 From: soergeld Date: Tue, 28 Apr 2020 13:09:04 +0200 Subject: [PATCH 30/47] trailing whitespace --- beets/autotag/hooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 81c2fadb1..2c78749e6 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -42,13 +42,13 @@ except AttributeError: class AttrDict(dict): """ Dictionary with flexible attributes - to get an tag value: + to get an tag value: value = info.tag or value = info[tag] or value = info.get(tag) or value = getattr(info, tag) all raise AttributeError when info doesn't have tag - to set a tag value: + to set a tag value: info.tag = value or info[tag] = value or setattr(info, tag, value) From d7ed84646ef2b5a91cda234c0284433ff6c67bd5 Mon Sep 17 00:00:00 2001 From: soergeld Date: Tue, 28 Apr 2020 14:18:27 +0200 Subject: [PATCH 31/47] create method for deepcopy --- beets/autotag/hooks.py | 14 +++++++++ test/test_autotag.py | 66 ++++-------------------------------------- 2 files changed, 20 insertions(+), 60 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 2c78749e6..444ae7b59 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -148,6 +148,16 @@ class AlbumInfo(AttrDict): for track in self.tracks: track.decode(codec) + def dup_albuminfo(self): + dupe = AlbumInfo() + dupe.update(self) + if 'tracks' in self: + tracks = [] + for track in self.tracks: + tracks.append(track.dup_trackinfo()) + dupe.tracks = tracks + return dupe + class TrackInfo(AttrDict): """Describes a canonical track present on a release. Appears as part @@ -209,6 +219,10 @@ class TrackInfo(AttrDict): if isinstance(value, bytes): setattr(self, fld, value.decode(codec, 'ignore')) + def dup_trackinfo(self): + dupe = TrackInfo() + dupe.update(self) + # Candidate distance scoring. diff --git a/test/test_autotag.py b/test/test_autotag.py index 06e6b7e72..cef6364d0 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -753,32 +753,14 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def test_album_artist_overrides_empty_track_artist(self): # make a deepcopy of self.info - trackinfo = [] - track = TrackInfo() - track.update(self.info.tracks[0]) - trackinfo.append(track) - track = TrackInfo() - track.update(self.info.tracks[1]) - trackinfo.append(track) - my_info = AlbumInfo() - my_info.update(self.info) - my_info.tracks = trackinfo + my_info = self.info.dup_albuminfo() self._apply(info=my_info) self.assertEqual(self.items[0].artist, 'artistNew') self.assertEqual(self.items[1].artist, 'artistNew') def test_album_artist_overridden_by_nonempty_track_artist(self): # make a deepcopy of self.info - trackinfo = [] - track = TrackInfo() - track.update(self.info.tracks[0]) - trackinfo.append(track) - track = TrackInfo() - track.update(self.info.tracks[1]) - trackinfo.append(track) - my_info = AlbumInfo() - my_info.update(self.info) - my_info.tracks = trackinfo + my_info = self.info.dup_albuminfo() my_info.tracks[0].artist = 'artist1!' my_info.tracks[1].artist = 'artist2!' self._apply(info=my_info) @@ -801,16 +783,7 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def test_full_date_applied(self): # make a deepcopy of self.info - trackinfo = [] - track = TrackInfo() - track.update(self.info.tracks[0]) - trackinfo.append(track) - track = TrackInfo() - track.update(self.info.tracks[1]) - trackinfo.append(track) - my_info = AlbumInfo() - my_info.update(self.info) - my_info.tracks = trackinfo + my_info = self.info.dup_albuminfo() my_info.year = 2013 my_info.month = 12 my_info.day = 18 @@ -826,16 +799,7 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): self.items.append(Item(year=4, month=5, day=6)) # make a deepcopy of self.info - trackinfo = [] - track = TrackInfo() - track.update(self.info.tracks[0]) - trackinfo.append(track) - track = TrackInfo() - track.update(self.info.tracks[1]) - trackinfo.append(track) - my_info = AlbumInfo() - my_info.update(self.info) - my_info.tracks = trackinfo + my_info = self.info.dup_albuminfo() my_info.year = 2013 self._apply(info=my_info) @@ -856,16 +820,7 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): def test_data_source_applied(self): # make a deepcopy of self.info - trackinfo = [] - track = TrackInfo() - track.update(self.info.tracks[0]) - trackinfo.append(track) - track = TrackInfo() - track.update(self.info.tracks[1]) - trackinfo.append(track) - my_info = AlbumInfo() - my_info.update(self.info) - my_info.tracks = trackinfo + my_info = self.info.dup_albuminfo() my_info.data_source = 'MusicBrainz' self._apply(info=my_info) @@ -928,16 +883,7 @@ class ApplyCompilationTest(_common.TestCase, ApplyTestUtil): def test_va_flag_sets_comp(self): # make a deepcopy of self.info - trackinfo = [] - track = TrackInfo() - track.update(self.info.tracks[0]) - trackinfo.append(track) - track = TrackInfo() - track.update(self.info.tracks[1]) - trackinfo.append(track) - va_info = AlbumInfo() - va_info.update(self.info) - va_info.tracks = trackinfo + va_info = self.info.dup_albuminfo() va_info.va = True self._apply(info=va_info) self.assertTrue(self.items[0].comp) From 370df6253d4c4da48183a018332c8c2778877979 Mon Sep 17 00:00:00 2001 From: soergeld Date: Tue, 28 Apr 2020 14:19:59 +0200 Subject: [PATCH 32/47] forgot a return --- beets/autotag/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 444ae7b59..c2775bf32 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -222,6 +222,7 @@ class TrackInfo(AttrDict): def dup_trackinfo(self): dupe = TrackInfo() dupe.update(self) + return dupe # Candidate distance scoring. From 499fcb8315e40e1554cfef5ccb4e8437fbfeb5b8 Mon Sep 17 00:00:00 2001 From: Joris Date: Sun, 3 May 2020 12:56:54 +0200 Subject: [PATCH 33/47] Update docs/plugins/subsonicplaylist.rst Co-authored-by: Adrian Sampson --- docs/plugins/subsonicplaylist.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/plugins/subsonicplaylist.rst b/docs/plugins/subsonicplaylist.rst index 22fe16816..c712a4823 100644 --- a/docs/plugins/subsonicplaylist.rst +++ b/docs/plugins/subsonicplaylist.rst @@ -23,11 +23,11 @@ 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" - 'delete': False, - 'playlist_ids': [], - 'playlist_names': [], - 'username': '', - 'password': '' + base_url: "https://your.subsonic.server" + delete: no + playlist_ids: [] + playlist_names: [] + username: '' + password: '' -Parameters `base_url`, `username` and `password` must be defined! \ No newline at end of file +Parameters `base_url`, `username` and `password` must be defined! From 82f0c59f46d6a0bd164313bc83540d2c4dcc442b Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 3 May 2020 13:10:24 +0200 Subject: [PATCH 34/47] review: updated subsonicplaylist.rst --- docs/plugins/subsonicplaylist.rst | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/plugins/subsonicplaylist.rst b/docs/plugins/subsonicplaylist.rst index c712a4823..b9a54c3de 100644 --- a/docs/plugins/subsonicplaylist.rst +++ b/docs/plugins/subsonicplaylist.rst @@ -2,11 +2,11 @@ Subsonic Playlist Plugin ======================== 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 +This is done by retrieving the track info from the subsonic server, searching +for 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" + subsonic_playlist: ";first playlist;second playlist;" To get all items in a playlist use the query `;playlist name;`. @@ -15,12 +15,22 @@ 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. +Next, configure the plugin to connect to your Subsonic server, like this:: + + subsonicplaylist: + base_url: http://subsonic.example.com + username: someUser + password: somePassword + +After this you can import your playlists by invoking the `subsonicplaylist` command. + 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:: + +Here's an example configuration with all the available options and their default values:: subsonicplaylist: base_url: "https://your.subsonic.server" From cb7dfe3f6f9fa37045cf6741e6909003784c62b0 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 3 May 2020 14:05:22 +0200 Subject: [PATCH 35/47] review: updated subsonicplaylist.py --- beetsplug/subsonicplaylist.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index fbbcd8ac3..561b6032d 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -19,7 +19,13 @@ from hashlib import md5 import xml.etree.ElementTree as ET import random import string +from urllib.parse import urlencode + import requests +from beets.dbcore.query import SubstringQuery + +from beets.dbcore import AndQuery, Query, MatchQuery + from beets import plugins from beets.ui import Subcommand @@ -46,19 +52,21 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def update_tags(self, playlist_dict, lib): for query, playlist_tag in playlist_dict.items(): - query = 'artist:"{}" album:"{}" title:"{}"'.format(*query) + query = AndQuery([SubstringQuery("artist", query[0]), + SubstringQuery("album", query[1]), + SubstringQuery("title", query[2])]) items = lib.items(query) - if len(items) <= 0: + if not items: self._log.warn(u"{} | track not found ({})", playlist_tag, query) continue for item in items: - item.update({'subsonic_playlist': playlist_tag}) + item.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 + xml = self.send('getPlaylist', {'id': 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' @@ -72,7 +80,7 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def commands(self): def build_playlist(lib, opts, args): - self._parse_opts(opts) + self.config.set_args(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)[ @@ -102,8 +110,6 @@ 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' @@ -117,24 +123,18 @@ class SubsonicPlaylistPlugin(BeetsPlugin): 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( (self.config['password'].get() + salt).encode()).hexdigest(), salt - def send(self, endpoint, params=''): + def send(self, endpoint, params=None): + if params is None: + params = dict() 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) + resp = requests.get(url + urlencode(params)) return resp From 13b4a1413d1c69e79dcb497fb32675c7d96fb302 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 3 May 2020 14:58:34 +0200 Subject: [PATCH 36/47] update all songs in one transaction --- beetsplug/subsonicplaylist.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 561b6032d..9c9c74974 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -51,18 +51,18 @@ class SubsonicPlaylistPlugin(BeetsPlugin): self.config['password'].redact = True def update_tags(self, playlist_dict, lib): - for query, playlist_tag in playlist_dict.items(): - query = AndQuery([SubstringQuery("artist", query[0]), - SubstringQuery("album", query[1]), - SubstringQuery("title", query[2])]) - items = lib.items(query) - if not items: - self._log.warn(u"{} | track not found ({})", playlist_tag, - query) - continue - for item in items: - item.subsonic_playlist = playlist_tag - with lib.transaction(): + with lib.transaction(): + for query, playlist_tag in playlist_dict.items(): + query = AndQuery([SubstringQuery("artist", query[0]), + SubstringQuery("album", query[1]), + SubstringQuery("title", query[2])]) + items = lib.items(query) + if not items: + self._log.warn(u"{} | track not found ({})", playlist_tag, + query) + continue + for item in items: + item.subsonic_playlist = playlist_tag item.try_sync(write=True, move=False) def get_playlist(self, playlist_id): From cb7ad191a35d6deda8872f633a7a44c8b26673d4 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 3 May 2020 15:02:51 +0200 Subject: [PATCH 37/47] removed duplicate line from merge --- docs/changelog.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index cbd9f3ce2..88d289e35 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,7 +7,6 @@ Changelog New features: * :doc:`/plugins/subsonicplaylist`: import playlist from a subsonic server. -* :doc:`plugins/discogs` now adds two extra fields: `discogs_labelid` and * 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 From 5c3debe2369cc161c2c9a875e3f2c20d8a2f5903 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 3 May 2020 16:07:27 +0200 Subject: [PATCH 38/47] removed unused imports --- beetsplug/subsonicplaylist.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 9c9c74974..14cd855b1 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -24,9 +24,7 @@ from urllib.parse import urlencode import requests from beets.dbcore.query import SubstringQuery -from beets.dbcore import AndQuery, Query, MatchQuery - -from beets import plugins +from beets.dbcore import AndQuery from beets.ui import Subcommand From a553693677e8cc11c645efa94d11b1669fd0e35c Mon Sep 17 00:00:00 2001 From: pants108 Date: Sun, 3 May 2020 00:06:55 +0000 Subject: [PATCH 39/47] fetchart: clean up invalid tmp files --- beetsplug/fetchart.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 763440238..d4ab714de 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -210,6 +210,9 @@ class ArtSource(RequestMixin): def fetch_image(self, candidate, plugin): raise NotImplementedError() + def cleanup_tmp(self, candidate): + raise NotImplementedError() + class LocalArtSource(ArtSource): IS_LOCAL = True @@ -218,6 +221,10 @@ class LocalArtSource(ArtSource): def fetch_image(self, candidate, plugin): pass + def cleanup_tmp(self, candidate): + # local art source does not create tmp files + pass + class RemoteArtSource(ArtSource): IS_LOCAL = False @@ -291,6 +298,13 @@ class RemoteArtSource(ArtSource): self._log.debug(u'error fetching art: {}', exc) return + def cleanup_tmp(self, candidate): + if candidate.path: + try: + util.remove(path=candidate.path) + except util.FilesystemError as exc: + self._log.debug(u'error cleaning up tmp art: {}', exc) + class CoverArtArchive(RemoteArtSource): NAME = u"Cover Art Archive" @@ -1017,6 +1031,8 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): u'using {0.LOC_STR} image {1}'.format( source, util.displayable_path(out.path))) break + # else: remove tmp images created by this invalid candidate + source.cleanup_tmp(candidate) if out: break From 4102aae100d07344a67432ee00dfe8ef30f3743b Mon Sep 17 00:00:00 2001 From: Joris Date: Tue, 5 May 2020 10:45:43 +0200 Subject: [PATCH 40/47] Update docs/plugins/subsonicplaylist.rst Co-authored-by: Adrian Sampson --- docs/plugins/subsonicplaylist.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/subsonicplaylist.rst b/docs/plugins/subsonicplaylist.rst index b9a54c3de..98c83ebe1 100644 --- a/docs/plugins/subsonicplaylist.rst +++ b/docs/plugins/subsonicplaylist.rst @@ -40,4 +40,4 @@ Here's an example configuration with all the available options and their default username: '' password: '' -Parameters `base_url`, `username` and `password` must be defined! +The `base_url`, `username`, and `password` options are required. From 1a79ae5a9f511394ed5767e270bd9737110717f6 Mon Sep 17 00:00:00 2001 From: pants108 Date: Tue, 5 May 2020 18:39:05 +0000 Subject: [PATCH 41/47] code review 1 --- beetsplug/fetchart.py | 14 +++++--------- docs/changelog.rst | 4 +++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index d4ab714de..70477a624 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -210,8 +210,8 @@ class ArtSource(RequestMixin): def fetch_image(self, candidate, plugin): raise NotImplementedError() - def cleanup_tmp(self, candidate): - raise NotImplementedError() + def cleanup(self, candidate): + pass class LocalArtSource(ArtSource): @@ -221,10 +221,6 @@ class LocalArtSource(ArtSource): def fetch_image(self, candidate, plugin): pass - def cleanup_tmp(self, candidate): - # local art source does not create tmp files - pass - class RemoteArtSource(ArtSource): IS_LOCAL = False @@ -298,7 +294,7 @@ class RemoteArtSource(ArtSource): self._log.debug(u'error fetching art: {}', exc) return - def cleanup_tmp(self, candidate): + def cleanup(self, candidate): if candidate.path: try: util.remove(path=candidate.path) @@ -1031,8 +1027,8 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): u'using {0.LOC_STR} image {1}'.format( source, util.displayable_path(out.path))) break - # else: remove tmp images created by this invalid candidate - source.cleanup_tmp(candidate) + # Remove temporary files for invalid candidates. + source.cleanup(candidate) if out: break diff --git a/docs/changelog.rst b/docs/changelog.rst index 5d8c26bb9..c09b1b8ab 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -122,9 +122,11 @@ New features: Fixes: -* :doc:`/plugins/fetchart`: Fixed a bug that caused fetchart to not take +* :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/fetchart`: Temporary files for fetched album art that fail + validation are now removed * :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 7c71bb87a2f549c063af979b50e2160cd32058ed Mon Sep 17 00:00:00 2001 From: soergeld Date: Fri, 8 May 2020 16:32:12 +0200 Subject: [PATCH 42/47] cleaning up, renaming dup_XXInfo() to copy() --- beets/autotag/hooks.py | 23 +++++-------- beetsplug/discogs.py | 19 ++++------ test/test_autotag.py | 78 +++++++++++++++--------------------------- test/test_ui.py | 5 +-- 4 files changed, 44 insertions(+), 81 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index c2775bf32..6e0cada53 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -80,9 +80,9 @@ class AlbumInfo(AttrDict): ``mediums`` along with the fields up through ``tracks`` are required. The others are optional and may be None. """ - def __init__(self, album=None, album_id=None, artist=None, artist_id=None, - tracks=None, asin=None, albumtype=None, va=False, year=None, - month=None, day=None, label=None, mediums=None, + def __init__(self, tracks, album=None, album_id=None, artist=None, + artist_id=None, asin=None, albumtype=None, va=False, + year=None, month=None, day=None, label=None, mediums=None, artist_sort=None, releasegroup_id=None, catalognum=None, script=None, language=None, country=None, style=None, genre=None, albumstatus=None, media=None, albumdisambig=None, @@ -144,18 +144,13 @@ class AlbumInfo(AttrDict): if isinstance(value, bytes): setattr(self, fld, value.decode(codec, 'ignore')) - if 'tracks' in self: - for track in self.tracks: - track.decode(codec) + for track in self.tracks: + track.decode(codec) - def dup_albuminfo(self): - dupe = AlbumInfo() + def copy(self): + dupe = AlbumInfo([]) dupe.update(self) - if 'tracks' in self: - tracks = [] - for track in self.tracks: - tracks.append(track.dup_trackinfo()) - dupe.tracks = tracks + dupe.tracks = [track.copy() for track in self.tracks] return dupe @@ -219,7 +214,7 @@ class TrackInfo(AttrDict): if isinstance(value, bytes): setattr(self, fld, value.decode(codec, 'ignore')) - def dup_trackinfo(self): + def copy(self): dupe = TrackInfo() dupe.update(self) return dupe diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index a0a6ea654..038fa809a 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -357,17 +357,13 @@ class DiscogsPlugin(BeetsPlugin): original_year = self.get_master_year(master_id) if master_id else year return AlbumInfo(album=album, album_id=album_id, artist=artist, - artist_id=artist_id, tracks=tracks, asin=None, - albumtype=albumtype, va=va, year=year, month=None, - day=None, label=label, mediums=len(set(mediums)), - artist_sort=None, releasegroup_id=master_id, - catalognum=catalogno, script=None, language=None, + artist_id=artist_id, tracks=tracks, + albumtype=albumtype, va=va, year=year, + label=label, mediums=len(set(mediums)), + releasegroup_id=master_id, catalognum=catalogno, country=country, style=style, genre=genre, - albumstatus=None, media=media, - albumdisambig=None, artist_credit=None, - original_year=original_year, original_month=None, - original_day=None, data_source='Discogs', - data_url=data_url, + media=media, original_year=original_year, + data_source='Discogs', data_url=data_url, discogs_albumid=discogs_albumid, discogs_labelid=labelid, discogs_artistid=artist_id) @@ -570,8 +566,7 @@ class DiscogsPlugin(BeetsPlugin): length = self.get_track_length(track['duration']) return TrackInfo(title=title, track_id=track_id, artist=artist, artist_id=artist_id, length=length, index=index, - medium=medium, medium_index=medium_index, - artist_sort=None, disctitle=None, artist_credit=None) + medium=medium, medium_index=medium_index) def get_track_index(self, position): """Returns the medium, medium index and subtrack index for a discogs diff --git a/test/test_autotag.py b/test/test_autotag.py index cef6364d0..febd1641d 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -350,9 +350,7 @@ class AlbumDistanceTest(_common.TestCase): artist=u'some artist', album=u'some album', tracks=_make_trackinfo(), - va=False, - album_id=None, - artist_id=None, + va=False ) self.assertEqual(self._dist(items, info), 0) @@ -364,9 +362,7 @@ class AlbumDistanceTest(_common.TestCase): artist=u'some artist', album=u'some album', tracks=_make_trackinfo(), - va=False, - album_id=None, - artist_id=None, + va=False ) dist = self._dist(items, info) self.assertNotEqual(dist, 0) @@ -382,9 +378,7 @@ class AlbumDistanceTest(_common.TestCase): artist=u'someone else', album=u'some album', tracks=_make_trackinfo(), - va=False, - album_id=None, - artist_id=None, + va=False ) self.assertNotEqual(self._dist(items, info), 0) @@ -397,9 +391,7 @@ class AlbumDistanceTest(_common.TestCase): artist=u'should be ignored', album=u'some album', tracks=_make_trackinfo(), - va=True, - album_id=None, - artist_id=None, + va=True ) self.assertEqual(self._dist(items, info), 0) @@ -413,9 +405,7 @@ class AlbumDistanceTest(_common.TestCase): artist=u'should be ignored', album=u'some album', tracks=_make_trackinfo(), - va=True, - album_id=None, - artist_id=None, + va=True ) info.tracks[0].artist = None info.tracks[1].artist = None @@ -431,9 +421,7 @@ class AlbumDistanceTest(_common.TestCase): artist=u'some artist', album=u'some album', tracks=_make_trackinfo(), - va=True, - album_id=None, - artist_id=None, + va=True ) self.assertNotEqual(self._dist(items, info), 0) @@ -446,9 +434,7 @@ class AlbumDistanceTest(_common.TestCase): artist=u'some artist', album=u'some album', tracks=_make_trackinfo(), - va=False, - album_id=None, - artist_id=None, + va=False ) dist = self._dist(items, info) self.assertTrue(0 < dist < 0.2) @@ -462,9 +448,7 @@ class AlbumDistanceTest(_common.TestCase): artist=u'some artist', album=u'some album', tracks=_make_trackinfo(), - va=False, - album_id=None, - artist_id=None, + va=False ) info.tracks[0].medium_index = 1 info.tracks[1].medium_index = 2 @@ -481,9 +465,7 @@ class AlbumDistanceTest(_common.TestCase): artist=u'some artist', album=u'some album', tracks=_make_trackinfo(), - va=False, - album_id=None, - artist_id=None, + va=False ) info.tracks[0].medium_index = 1 info.tracks[1].medium_index = 2 @@ -505,9 +487,9 @@ class AssignmentTest(unittest.TestCase): items.append(self.item(u'three', 2)) items.append(self.item(u'two', 3)) trackinfo = [] - trackinfo.append(TrackInfo(title=u'one', track_id=None)) - trackinfo.append(TrackInfo(title=u'two', track_id=None)) - trackinfo.append(TrackInfo(title=u'three', track_id=None)) + trackinfo.append(TrackInfo(title=u'one')) + trackinfo.append(TrackInfo(title=u'two')) + trackinfo.append(TrackInfo(title=u'three')) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, []) @@ -524,9 +506,9 @@ class AssignmentTest(unittest.TestCase): items.append(self.item(u'three', 1)) items.append(self.item(u'two', 1)) trackinfo = [] - trackinfo.append(TrackInfo(title=u'one', track_id=None)) - trackinfo.append(TrackInfo(title=u'two', track_id=None)) - trackinfo.append(TrackInfo(title=u'three', track_id=None)) + trackinfo.append(TrackInfo(title=u'one')) + trackinfo.append(TrackInfo(title=u'two')) + trackinfo.append(TrackInfo(title=u'three')) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, []) @@ -542,9 +524,9 @@ class AssignmentTest(unittest.TestCase): items.append(self.item(u'one', 1)) items.append(self.item(u'three', 3)) trackinfo = [] - trackinfo.append(TrackInfo(title=u'one', track_id=None)) - trackinfo.append(TrackInfo(title=u'two', track_id=None)) - trackinfo.append(TrackInfo(title=u'three', track_id=None)) + trackinfo.append(TrackInfo(title=u'one')) + trackinfo.append(TrackInfo(title=u'two')) + trackinfo.append(TrackInfo(title=u'three')) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, []) @@ -560,8 +542,8 @@ class AssignmentTest(unittest.TestCase): items.append(self.item(u'two', 2)) items.append(self.item(u'three', 3)) trackinfo = [] - trackinfo.append(TrackInfo(title=u'one', track_id=None)) - trackinfo.append(TrackInfo(title=u'three', track_id=None)) + trackinfo.append(TrackInfo(title=u'one')) + trackinfo.append(TrackInfo(title=u'three')) mapping, extra_items, extra_tracks = \ match.assign_items(items, trackinfo) self.assertEqual(extra_items, [items[1]]) @@ -597,7 +579,7 @@ class AssignmentTest(unittest.TestCase): items.append(item(12, 186.45916150485752)) def info(index, title, length): - return TrackInfo(title=title, track_id=None, length=length, + return TrackInfo(title=title, length=length, index=index) trackinfo = [] trackinfo.append(info(1, u'Alone', 238.893)) @@ -752,15 +734,13 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): self.assertEqual(self.items[1].albumtype, 'album') def test_album_artist_overrides_empty_track_artist(self): - # make a deepcopy of self.info - my_info = self.info.dup_albuminfo() + my_info = self.info.copy() self._apply(info=my_info) self.assertEqual(self.items[0].artist, 'artistNew') self.assertEqual(self.items[1].artist, 'artistNew') def test_album_artist_overridden_by_nonempty_track_artist(self): - # make a deepcopy of self.info - my_info = self.info.dup_albuminfo() + my_info = self.info.copy() my_info.tracks[0].artist = 'artist1!' my_info.tracks[1].artist = 'artist2!' self._apply(info=my_info) @@ -782,8 +762,7 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): self.assertEqual(self.items[1].artist_sort, 'albumArtistSort') def test_full_date_applied(self): - # make a deepcopy of self.info - my_info = self.info.dup_albuminfo() + my_info = self.info.copy() my_info.year = 2013 my_info.month = 12 my_info.day = 18 @@ -798,8 +777,7 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): self.items.append(Item(year=1, month=2, day=3)) self.items.append(Item(year=4, month=5, day=6)) - # make a deepcopy of self.info - my_info = self.info.dup_albuminfo() + my_info = self.info.copy() my_info.year = 2013 self._apply(info=my_info) @@ -819,8 +797,7 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): self.assertEqual(self.items[0].day, 3) def test_data_source_applied(self): - # make a deepcopy of self.info - my_info = self.info.dup_albuminfo() + my_info = self.info.copy() my_info.data_source = 'MusicBrainz' self._apply(info=my_info) @@ -882,8 +859,7 @@ class ApplyCompilationTest(_common.TestCase, ApplyTestUtil): self.assertFalse(self.items[1].comp) def test_va_flag_sets_comp(self): - # make a deepcopy of self.info - va_info = self.info.dup_albuminfo() + va_info = self.info.copy() va_info.va = True self._apply(info=va_info) self.assertTrue(self.items[0].comp) diff --git a/test/test_ui.py b/test/test_ui.py index d0be10060..b1e7e8fad 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -1138,10 +1138,7 @@ class SummarizeItemsTest(_common.TestCase): self.assertEqual(summary, u"1 items, F, 4kbps, 10:54, 987.0 B") # make a copy of self.item - i2 = library.Item() - i2.bitrate = 4321 - i2.length = 10 * 60 + 54 - i2.format = "F" + i2 = self.item.copy() summary = commands.summarize_items([self.item, i2], False) self.assertEqual(summary, u"2 items, F, 4kbps, 21:48, 1.9 KiB") From 4933671c10b955a6ce7a732b520d0b6c4a9994a0 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Fri, 8 May 2020 21:36:56 +0200 Subject: [PATCH 43/47] review: updated subsonicplaylist.py --- beetsplug/subsonicplaylist.py | 77 ++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 14cd855b1..d02a8919d 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -15,24 +15,44 @@ from __future__ import absolute_import, division, print_function -from hashlib import md5 -import xml.etree.ElementTree as ET import random import string +import xml.etree.ElementTree as ET +from hashlib import md5 from urllib.parse import urlencode import requests -from beets.dbcore.query import SubstringQuery from beets.dbcore import AndQuery - +from beets.dbcore.query import SubstringQuery +from beets.plugins import BeetsPlugin from beets.ui import Subcommand -from beets.plugins import BeetsPlugin - __author__ = 'https://github.com/MrNuggelz' +def filter_to_be_removed(items, keys): + if len(items) > len(keys): + dont_remove = [] + for artist, album, title in keys: + for item in items: + if artist == item['artist'] and \ + album == item['album'] and \ + title == item['title']: + dont_remove.append(item) + return [item for item in items if item not in dont_remove] + else: + def to_be_removed(item): + for artist, album, title in keys: + if artist == item['artist'] and\ + album == item['album'] and\ + title == item['title']: + return False + return True + + return [item for item in items if to_be_removed(item)] + + class SubsonicPlaylistPlugin(BeetsPlugin): def __init__(self): @@ -80,12 +100,12 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def build_playlist(lib, opts, args): self.config.set_args(opts) ids = self.config['playlist_ids'].as_str_seq() - if len(self.config['playlist_names'].as_str_seq()) > 0: + if self.config['playlist_names'].as_str_seq(): 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' + ' but no error message found' self._log.warn( playlists.attrib.get('message', alt_error)) return @@ -93,17 +113,17 @@ class SubsonicPlaylistPlugin(BeetsPlugin): for playlist in playlists: if name == playlist.attrib['name']: 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 + ';' + + playlist_dict = self.get_playlists(ids) + # delete old tags if self.config['delete']: - for item in lib.items('subsonic_playlist:";"'): - item.update({'subsonic_playlist': ''}) + existing = list(lib.items('subsonic_playlist:";"')) + to_be_removed = filter_to_be_removed( + existing, + playlist_dict.keys()) + for item in to_be_removed: + item['subsonic_playlist'] = '' with lib.transaction(): item.try_sync(write=True, move=False) @@ -129,10 +149,21 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def send(self, endpoint, params=None): if params is None: params = dict() - 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 + urlencode(params)) + a, b = self.generate_token() + params['u'] = self.config['username'] + params['t'] = a + params['s'] = b + params['v'] = '1.12.0' + params['c'] = 'beets' + resp = requests.get('{}/rest/{}?{}'.format(self.config['base_url'].get(),endpoint,urlencode(params))) return resp + + def get_playlists(self, ids): + output = dict() + for playlist_id in ids: + name, tracks = self.get_playlist(playlist_id) + for track in tracks: + if track not in output: + output[track] = ';' + output[track] += name + ';' + return output \ No newline at end of file From 4b7f42d21458166ef5eefb9b3068d8ff0447016c Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Sat, 9 May 2020 12:29:52 +0200 Subject: [PATCH 44/47] Update AttrDict docstring Co-authored-by: Adrian Sampson --- beets/autotag/hooks.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 6e0cada53..9bdf6b001 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -40,18 +40,8 @@ except AttributeError: # Classes used to represent candidate options. class AttrDict(dict): - """ - Dictionary with flexible attributes - to get an tag value: - value = info.tag - or value = info[tag] - or value = info.get(tag) - or value = getattr(info, tag) - all raise AttributeError when info doesn't have tag - to set a tag value: - info.tag = value - or info[tag] = value - or setattr(info, tag, value) + """A dictionary that supports attribute ("dot") access, so `d.field` + is equivalent to `d['field']`. """ def __getattr__(self, attr): From 8fa103e0de4de0ebb01685345f44e229cf4dab21 Mon Sep 17 00:00:00 2001 From: soergeld Date: Sat, 9 May 2020 12:44:36 +0200 Subject: [PATCH 45/47] changelog entry --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index e2f1521eb..5434e8b13 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -115,6 +115,9 @@ New features: :bug:`3459` * :doc:`/plugins/fetchart`: Album art can now be fetched from `last.fm`_. :bug:`3530` +* The classes ``AlbumInfo`` and ``TrackInfo`` now have flexible attributes, + allowing to solve :bug:`1547`. + Thanks to :user:`dosoe`. Fixes: From 5d90296a20b336349261e1d39a95503a605112f9 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sat, 9 May 2020 13:16:56 +0200 Subject: [PATCH 46/47] fixed flake8 issues --- beetsplug/subsonicplaylist.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index d02a8919d..95bdaa886 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -155,7 +155,11 @@ class SubsonicPlaylistPlugin(BeetsPlugin): params['s'] = b params['v'] = '1.12.0' params['c'] = 'beets' - resp = requests.get('{}/rest/{}?{}'.format(self.config['base_url'].get(),endpoint,urlencode(params))) + resp = requests.get('{}/rest/{}?{}'.format( + self.config['base_url'].get(), + endpoint, + urlencode(params)) + ) return resp def get_playlists(self, ids): @@ -166,4 +170,4 @@ class SubsonicPlaylistPlugin(BeetsPlugin): if track not in output: output[track] = ';' output[track] += name + ';' - return output \ No newline at end of file + return output From fd1fd2182b3ee3af6aa775f3127ade69594ac1d4 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Mon, 11 May 2020 20:09:04 +0200 Subject: [PATCH 47/47] changed "SubstringQuery" to "MatchQuery" to prevent wrong song selection --- beetsplug/subsonicplaylist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 95bdaa886..c537c4093 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -24,7 +24,7 @@ from urllib.parse import urlencode import requests from beets.dbcore import AndQuery -from beets.dbcore.query import SubstringQuery +from beets.dbcore.query import MatchQuery from beets.plugins import BeetsPlugin from beets.ui import Subcommand @@ -71,9 +71,9 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def update_tags(self, playlist_dict, lib): with lib.transaction(): for query, playlist_tag in playlist_dict.items(): - query = AndQuery([SubstringQuery("artist", query[0]), - SubstringQuery("album", query[1]), - SubstringQuery("title", query[2])]) + query = AndQuery([MatchQuery("artist", query[0]), + MatchQuery("album", query[1]), + MatchQuery("title", query[2])]) items = lib.items(query) if not items: self._log.warn(u"{} | track not found ({})", playlist_tag,