From cc6080fa21ecc85a4f1500cd2cc2ae9d10731354 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Sun, 12 Oct 2014 22:44:34 +0200 Subject: [PATCH 01/12] ftintitle: add 'auto' option --- beetsplug/ftintitle.py | 65 ++++++++++++++------------------------ docs/plugins/ftintitle.rst | 2 ++ 2 files changed, 26 insertions(+), 41 deletions(-) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index 831236b3c..dd3460fd8 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -16,10 +16,12 @@ """ from beets.plugins import BeetsPlugin from beets import ui -from beets.util import displayable_path from beets import config +import logging import re +log = logging.getLogger('beets') + def split_on_feat(artist): """Given an artist string, split the "main" artist from any artist @@ -69,55 +71,30 @@ def update_metadata(item, feat_part, drop_feat): item.title = new_title -def ft_in_title(item, drop_feat): +def ft_in_title(item, drop_feat, write): """Look for featured artists in the item's artist fields and move them to the title. """ artist = item.artist.strip() - albumartist = item.albumartist.strip() - # Check whether there is a featured artist on this track and the - # artist field does not exactly match the album artist field. In - # that case, we attempt to move the featured artist to the title. - _, featured = split_on_feat(artist) - if featured and albumartist != artist and albumartist: - ui.print_(displayable_path(item.path)) - feat_part = None + _, feat_part = split_on_feat(artist) - # Look for the album artist in the artist field. If it's not - # present, give up. - albumartist_split = artist.split(albumartist) - if len(albumartist_split) <= 1: - ui.print_('album artist not present in artist') + if feat_part: + update_metadata(item, feat_part, drop_feat) + else: + ui.print_(u'no featuring artists found') - # If the last element of the split (the right-hand side of the - # album artist) is nonempty, then it probably contains the - # featured artist. - elif albumartist_split[-1] != '': - # Extract the featured artist from the right-hand side. - _, feat_part = split_on_feat(albumartist_split[-1]) - - # Otherwise, if there's nothing on the right-hand side, look for a - # featuring artist on the left-hand side. - else: - lhs, rhs = split_on_feat(albumartist_split[0]) - if rhs: - feat_part = lhs - - # If we have a featuring artist, move it to the title. - if feat_part: - update_metadata(item, feat_part, drop_feat) - else: - ui.print_(u'no featuring artists found') - - ui.print_() + if write: + item.try_write() + item.store() class FtInTitlePlugin(BeetsPlugin): def __init__(self): super(FtInTitlePlugin, self).__init__() - + self.import_stages = [self.imported] self.config.add({ + 'auto': True, 'drop': False }) @@ -138,10 +115,16 @@ class FtInTitlePlugin(BeetsPlugin): write = config['import']['write'].get(bool) for item in lib.items(ui.decargs(args)): - ft_in_title(item, drop_feat) - item.store() - if write: - item.try_write() + ft_in_title(item, drop_feat, write) self._command.func = func return [self._command] + + def imported(self, session, task): + """Import hook for moving featuring artist automatically. + """ + drop_feat = self.config['drop'].get(bool) + write = config['import']['write'].get(bool) + if self.config['auto'].get(bool): + for item in task.imported_items(): + ft_in_title(item, drop_feat, write) diff --git a/docs/plugins/ftintitle.rst b/docs/plugins/ftintitle.rst index ed13cb840..c8bf48f46 100644 --- a/docs/plugins/ftintitle.rst +++ b/docs/plugins/ftintitle.rst @@ -21,4 +21,6 @@ If you prefer to remove featured artists entirely instead of adding them to the title field, either use the ``-d`` flag to the command or set the ``ftintitle.drop`` config option. +To disable this plugin on import, set the ``auto`` config option to false. + .. _MusicBrainz style: http://musicbrainz.org/doc/Style From 740b510ed717f3d872f1912313a2b38e30fa4862 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Mon, 13 Oct 2014 21:21:28 +0200 Subject: [PATCH 02/12] restoring ft_in_title implementation --- beetsplug/ftintitle.py | 45 +++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index dd3460fd8..8fe70764d 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -16,6 +16,7 @@ """ from beets.plugins import BeetsPlugin from beets import ui +from beets.util import displayable_path from beets import config import logging import re @@ -71,22 +72,48 @@ def update_metadata(item, feat_part, drop_feat): item.title = new_title -def ft_in_title(item, drop_feat, write): +def ft_in_title(item, drop_feat): """Look for featured artists in the item's artist fields and move them to the title. """ artist = item.artist.strip() + albumartist = item.albumartist.strip() - _, feat_part = split_on_feat(artist) + # Check whether there is a featured artist on this track and the + # artist field does not exactly match the album artist field. In + # that case, we attempt to move the featured artist to the title. + _, featured = split_on_feat(artist) + if featured and albumartist != artist and albumartist: + ui.print_(displayable_path(item.path)) + feat_part = None - if feat_part: - update_metadata(item, feat_part, drop_feat) - else: - ui.print_(u'no featuring artists found') + # Look for the album artist in the artist field. If it's not + # present, give up. + albumartist_split = artist.split(albumartist) + if len(albumartist_split) <= 1: + ui.print_('album artist not present in artist') - if write: - item.try_write() - item.store() + # If the last element of the split (the right-hand side of the + # album artist) is nonempty, then it probably contains the + # featured artist. + elif albumartist_split[-1] != '': + # Extract the featured artist from the right-hand side. + _, feat_part = split_on_feat(albumartist_split[-1]) + + # Otherwise, if there's nothing on the right-hand side, look for a + # featuring artist on the left-hand side. + else: + lhs, rhs = split_on_feat(albumartist_split[0]) + if rhs: + feat_part = lhs + + # If we have a featuring artist, move it to the title. + if feat_part: + update_metadata(item, feat_part, drop_feat) + else: + ui.print_(u'no featuring artists found') + + ui.print_() class FtInTitlePlugin(BeetsPlugin): From 4884ae3c46bcd026dc19f48c9859675f889757d7 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Mon, 13 Oct 2014 22:25:09 +0200 Subject: [PATCH 03/12] register import hook only if needed --- beetsplug/ftintitle.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index 8fe70764d..f783cfa32 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -119,7 +119,7 @@ def ft_in_title(item, drop_feat): class FtInTitlePlugin(BeetsPlugin): def __init__(self): super(FtInTitlePlugin, self).__init__() - self.import_stages = [self.imported] + self.config.add({ 'auto': True, 'drop': False @@ -134,6 +134,9 @@ class FtInTitlePlugin(BeetsPlugin): action='store_true', default=False, help='drop featuring from artists and ignore title update') + if self.config['auto']: + self.import_stages = [self.imported] + def commands(self): def func(lib, opts, args): @@ -152,6 +155,6 @@ class FtInTitlePlugin(BeetsPlugin): """ drop_feat = self.config['drop'].get(bool) write = config['import']['write'].get(bool) - if self.config['auto'].get(bool): - for item in task.imported_items(): - ft_in_title(item, drop_feat, write) + + for item in task.imported_items(): + ft_in_title(item, drop_feat, write) From ac57ef0e67edca143e62f17f3492ed1d8022f29f Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Tue, 14 Oct 2014 20:58:36 +0200 Subject: [PATCH 04/12] restore write argument for ft_in_title --- beetsplug/ftintitle.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index f783cfa32..eaeb641e9 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -72,7 +72,7 @@ def update_metadata(item, feat_part, drop_feat): item.title = new_title -def ft_in_title(item, drop_feat): +def ft_in_title(item, drop_feat, write): """Look for featured artists in the item's artist fields and move them to the title. """ @@ -114,7 +114,9 @@ def ft_in_title(item, drop_feat): ui.print_(u'no featuring artists found') ui.print_() - + if write: + item.try_write() + item.store() class FtInTitlePlugin(BeetsPlugin): def __init__(self): From 343972f5d00ca12e374f094e39ee82fc735514cc Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Sat, 18 Oct 2014 11:04:10 +0200 Subject: [PATCH 05/12] move metadata management in command function --- beetsplug/ftintitle.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index eaeb641e9..8a92d20df 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -114,9 +114,7 @@ def ft_in_title(item, drop_feat, write): ui.print_(u'no featuring artists found') ui.print_() - if write: - item.try_write() - item.store() + class FtInTitlePlugin(BeetsPlugin): def __init__(self): @@ -147,7 +145,10 @@ class FtInTitlePlugin(BeetsPlugin): write = config['import']['write'].get(bool) for item in lib.items(ui.decargs(args)): - ft_in_title(item, drop_feat, write) + ft_in_title(item, drop_feat) + item.store() + if write: + item.try_write() self._command.func = func return [self._command] @@ -156,7 +157,6 @@ class FtInTitlePlugin(BeetsPlugin): """Import hook for moving featuring artist automatically. """ drop_feat = self.config['drop'].get(bool) - write = config['import']['write'].get(bool) for item in task.imported_items(): - ft_in_title(item, drop_feat, write) + ft_in_title(item, drop_feat) From 6aa9e60756ad03d8c7f696bb78e8de6c2e011565 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 24 Oct 2014 16:30:14 -0700 Subject: [PATCH 06/12] Add tests for #1029 The mbngs library does not like to be called with whitespace-only criteria. --- test/test_mb.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/test/test_mb.py b/test/test_mb.py index c8ffceefb..f41ec5510 100644 --- a/test/test_mb.py +++ b/test/test_mb.py @@ -18,6 +18,7 @@ import _common from _common import unittest from beets.autotag import mb from beets import config +import mock class MBAlbumInfoTest(_common.TestCase): @@ -407,6 +408,78 @@ class ArtistFlatteningTest(_common.TestCase): self.assertEqual(flat, ('ALIASfr_P', 'ALIASSORTfr_P', 'CREDIT')) +class MBLibraryTest(unittest.TestCase): + def test_match_track(self): + with mock.patch('musicbrainzngs.search_recordings') as p: + p.return_value = { + 'recording-list': [{ + 'title': 'foo', + 'id': 'bar', + 'length': 42, + }], + } + ti = list(mb.match_track('hello', 'there'))[0] + + p.assert_called_with(artist='hello', recording='there', limit=5) + self.assertEqual(ti.title, 'foo') + self.assertEqual(ti.track_id, 'bar') + + def test_match_album(self): + mbid = 'd2a6f856-b553-40a0-ac54-a321e8e2da99' + with mock.patch('musicbrainzngs.search_releases') as sp: + sp.return_value = { + 'release-list': [{ + 'id': mbid, + }], + } + with mock.patch('musicbrainzngs.get_release_by_id') as gp: + gp.return_value = { + 'release': { + 'title': 'hi', + 'id': mbid, + 'medium-list': [{ + 'track-list': [{ + 'recording': { + 'title': 'foo', + 'id': 'bar', + 'length': 42, + }, + 'position': 9, + }], + 'position': 5, + }], + 'artist-credit': [{ + 'artist': { + 'name': 'some-artist', + 'id': 'some-id', + }, + }], + 'release-group': { + 'id': 'another-id', + } + } + } + + ai = list(mb.match_album('hello', 'there'))[0] + + sp.assert_called_with(artist='hello', release='there', limit=5) + gp.assert_calledwith(mbid) + self.assertEqual(ai.tracks[0].title, 'foo') + self.assertEqual(ai.album, 'hi') + + def test_match_track_empty(self): + with mock.patch('musicbrainzngs.search_recordings') as p: + til = list(mb.match_track(' ', ' ')) + self.assertFalse(p.called) + self.assertEqual(til, []) + + def test_match_album_empty(self): + with mock.patch('musicbrainzngs.search_releases') as p: + ail = list(mb.match_album(' ', ' ')) + self.assertFalse(p.called) + self.assertEqual(ail, []) + + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From 51e57022cb05985f72b352ee96e02257348ec37c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 24 Oct 2014 16:32:04 -0700 Subject: [PATCH 07/12] Fix #1029: whitespace-only searches --- beets/autotag/mb.py | 8 ++++---- docs/changelog.rst | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 159d623d5..d063f6278 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -310,9 +310,9 @@ def match_album(artist, album, tracks=None, limit=SEARCH_LIMIT): optionally, a number of tracks on the album. """ # Build search criteria. - criteria = {'release': album.lower()} + criteria = {'release': album.lower().strip()} if artist is not None: - criteria['artist'] = artist.lower() + criteria['artist'] = artist.lower().strip() else: # Various Artists search. criteria['arid'] = VARIOUS_ARTISTS_ID @@ -341,8 +341,8 @@ def match_track(artist, title, limit=SEARCH_LIMIT): objects. May raise a MusicBrainzAPIError. """ criteria = { - 'artist': artist.lower(), - 'recording': title.lower(), + 'artist': artist.lower().strip(), + 'recording': title.lower().strip(), } if not any(criteria.itervalues()): diff --git a/docs/changelog.rst b/docs/changelog.rst index 38f99638d..d76eeeb69 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -44,6 +44,8 @@ Fixes: the Discogs servers. Thanks to Dustin Rodriguez. * :doc:`/plugins/embedart`: Do not log "embedding album art into..." messages during the import process. +* Fix a crash in the autotagger when files had only whitespace in their + metadata. 1.3.8 (September 17, 2014) From a7b7e234363e18d1fba880e9397c05218a12427c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 24 Oct 2014 16:46:55 -0700 Subject: [PATCH 08/12] Minor fixes for #1033 --- beetsplug/embedart.py | 6 +++--- beetsplug/fetchart.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 1112f6d69..361570d4f 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -41,7 +41,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): 'maxwidth': 0, 'auto': True, 'compare_threshold': 0, - 'ifempty': False + 'ifempty': False, }) if self.config['maxwidth'].get(int) and not ArtResizer.shared.local: @@ -64,7 +64,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): ) maxwidth = config['embedart']['maxwidth'].get(int) compare_threshold = config['embedart']['compare_threshold'].get(int) - ifempty = config['embedart']['ifempty'].get() + ifempty = config['embedart']['ifempty'].get(bool) def embed_func(lib, opts, args): if opts.file: @@ -161,7 +161,7 @@ def embed_album(album, maxwidth=None, quiet=False): for item in album.items(): embed_item(item, imagepath, maxwidth, None, config['embedart']['compare_threshold'].get(int), - config['embedart']['ifempty']) + config['embedart']['ifempty'].get(bool)) def check_art_similarity(item, imagepath, compare_threshold): diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 1474a7b09..81c984b60 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the From 798838a0f68fc1bf5ee7195fb1bf64177a86e7c5 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 24 Oct 2014 16:49:29 -0700 Subject: [PATCH 09/12] Changelog/docs for #1033/#1020 --- docs/changelog.rst | 2 ++ docs/plugins/embedart.rst | 5 ++++- docs/plugins/fetchart.rst | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d76eeeb69..b7d554504 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,8 @@ Features: in its album-level incarnation, it could not represent heterogeneous releases---for example, an album consisting of a CD and a DVD. Now, tracks accurately indicate the media they appear on. Thanks to Heinz Wiesinger. +* :doc:`/plugins/embedart`: A new ``ifempty`` config option lets you only + embed album art when no album art is present. Thanks to kerobaros. Fixes: diff --git a/docs/plugins/embedart.rst b/docs/plugins/embedart.rst index 5abc24a04..387d6a3ff 100644 --- a/docs/plugins/embedart.rst +++ b/docs/plugins/embedart.rst @@ -28,7 +28,7 @@ When importing a lot of files with the ``auto`` option, one may be reluctant to overwrite existing embedded art for all of them. You can tell beets to avoid embedding images that are too different from the -existing ones. +existing ones. This works by computing the perceptual hashes (`PHASH`_) of the two images and checking that the difference between the two does not exceed a threshold. You can set the threshold with the ``compare_threshold`` option. @@ -81,6 +81,9 @@ regarding to embedded art to be written to the file (see :ref:`image-similarity-check`). The default is 0 (no similarity check). Requires `ImageMagick`_. +To avoid embedding album art for files that already have album art, set the +``ifempty`` config option to ``yes``. + .. _PIL: http://www.pythonware.com/products/pil/ .. _ImageMagick: http://www.imagemagick.org/ .. _PHASH: http://www.fmwconcepts.com/misc_tests/perceptual_hash_test_results_510/ diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 37d1b9056..1aa171dac 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -111,7 +111,6 @@ your config file:: fetchart: google_search: true - Embedding Album Art ------------------- From c158bb630ca3028fbeb468caa2d107177e0e9df3 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 24 Oct 2014 17:15:09 -0700 Subject: [PATCH 10/12] Fix mock function restoration in test :( An argument for using decorators, context managers and stuff so this is impossible to mess up. --- test/test_importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_importer.py b/test/test_importer.py index 6d1b65dab..feafad923 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -65,7 +65,7 @@ class AutotagStub(object): def restore(self): autotag.mb.match_album = self.mb_match_album - autotag.mb.match_track = self.mb_match_album + autotag.mb.match_track = self.mb_match_track autotag.mb.album_for_id = self.mb_album_for_id autotag.mb.track_for_id = self.mb_track_for_id From 61bdbd6dd771632b69cfd75d47d4e4205b721de6 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 24 Oct 2014 17:19:42 -0700 Subject: [PATCH 11/12] Changelog for #1011 (fix #841) --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index b7d554504..c05d08bce 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,6 +20,8 @@ Features: accurately indicate the media they appear on. Thanks to Heinz Wiesinger. * :doc:`/plugins/embedart`: A new ``ifempty`` config option lets you only embed album art when no album art is present. Thanks to kerobaros. +* :doc:`/plugins/ftintitle`: The plugin now runs automatically on import. To + disable this, unset the ``auto`` config flag. Fixes: From 0325fe2225f05fe37cb53f7e7c080ec0eb7be05f Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 24 Oct 2014 17:33:11 -0700 Subject: [PATCH 12/12] lyrics: Remove script tags (fix #1034) --- beetsplug/lyrics.py | 6 +++--- test/test_lyrics.py | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 47e299f82..7dea01a32 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -329,10 +329,10 @@ def _scrape_strip_cruft(html, plain_text_out=False): """ html = unescape(html) - # Normalize EOL - html = html.replace('\r', '\n') + html = html.replace('\r', '\n') # Normalize EOL. html = re.sub(r' +', ' ', html) # Whitespaces collapse. - html = BREAK_RE.sub('\n', html) #
eats up surrounding '\n' + html = BREAK_RE.sub('\n', html) #
eats up surrounding '\n'. + html = re.sub(r'<(script).*?(?s)', '', html) # Strip script tags. if plain_text_out: # Strip remaining HTML tags html = TAG_RE.sub('', html) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 6b2929565..c4876c003 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2014, Fabrice Laporte. # @@ -131,7 +130,7 @@ class LyricsPluginTest(unittest.TestCase): self.assertFalse(lyrics.is_lyrics(t)) def test_slugify(self): - text = u"http://site.com/çafe-au_lait(boisson)" + text = u"http://site.com/\xe7afe-au_lait(boisson)" self.assertEqual(lyrics.slugify(text), 'http://site.com/cafe_au_lait') def test_scrape_strip_cruft(self): @@ -144,6 +143,11 @@ class LyricsPluginTest(unittest.TestCase): self.assertEqual(lyrics._scrape_strip_cruft(text, True), "one\ntwo !\n\nfour") + def test_scrape_strip_scripts(self): + text = u"""foobaz""" + self.assertEqual(lyrics._scrape_strip_cruft(text, True), + "foobaz") + def test_scrape_merge_paragraphs(self): text = u"one

two

three" self.assertEqual(lyrics._scrape_merge_paragraphs(text), @@ -263,7 +267,7 @@ class LyricsGooglePluginTest(unittest.TestCase): except ImportError: self.skipTest('Beautiful Soup 4 not available') if sys.version_info[:3] < (2, 7, 3): - self.skipTest("Python’s built-in HTML parser is not good enough") + self.skipTest("Python's built-in HTML parser is not good enough") lyrics.LyricsPlugin() lyrics.fetch_url = MockFetchUrl()