From 58417526cb1f871b5a1dcf0a7e03c46cf3064eca Mon Sep 17 00:00:00 2001 From: discopatrick Date: Thu, 20 Apr 2017 14:28:36 +0100 Subject: [PATCH 01/52] Rename `InvalidQueryArgumentTypeError` to `InvalidQueryArgumentValueError` The way we use `InvalidQueryArgumentTypeError` is more akin to a `ValueError` than a `TypeError`. For example, we try to parse a string as an int, float, or date, but the parsing fails - there was nothing wrong with the type of the variable (string), but its contents were not parseable into the type we wanted - there was a problem with the value of the string. --- beets/dbcore/query.py | 24 ++++++++++++------------ beets/library.py | 2 +- test/test_datequery.py | 8 ++++---- test/test_query.py | 6 +++--- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index aa8aa4af8..a6040c7d1 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -47,7 +47,7 @@ class InvalidQueryError(ParsingError): super(InvalidQueryError, self).__init__(message) -class InvalidQueryArgumentTypeError(ParsingError): +class InvalidQueryArgumentValueError(ParsingError): """Represent a query argument that could not be converted as expected. It exists to be caught in upper stack levels so a meaningful (i.e. with the @@ -57,7 +57,7 @@ class InvalidQueryArgumentTypeError(ParsingError): message = u"'{0}' is not {1}".format(what, expected) if detail: message = u"{0}: {1}".format(message, detail) - super(InvalidQueryArgumentTypeError, self).__init__(message) + super(InvalidQueryArgumentValueError, self).__init__(message) class Query(object): @@ -211,9 +211,9 @@ class RegexpQuery(StringFieldQuery): self.pattern = re.compile(self.pattern) except re.error as exc: # Invalid regular expression. - raise InvalidQueryArgumentTypeError(pattern, - u"a regular expression", - format(exc)) + raise InvalidQueryArgumentValueError(pattern, + u"a regular expression", + format(exc)) @staticmethod def _normalize(s): @@ -285,7 +285,7 @@ class NumericQuery(FieldQuery): try: return float(s) except ValueError: - raise InvalidQueryArgumentTypeError(s, u"an int or a float") + raise InvalidQueryArgumentValueError(s, u"an int or a float") def __init__(self, field, pattern, fast=True): super(NumericQuery, self).__init__(field, pattern, fast) @@ -548,7 +548,7 @@ class Period(object): @classmethod def parse(cls, string): """Parse a date and return a `Period` object, or `None` if the - string is empty, or raise an InvalidQueryArgumentTypeError if + string is empty, or raise an InvalidQueryArgumentValueError if the string could not be parsed to a date. """ if not string: @@ -556,15 +556,15 @@ class Period(object): ordinal = string.count('-') if ordinal >= len(cls.date_formats): # Too many components. - raise InvalidQueryArgumentTypeError(string, - 'a valid datetime string') + raise InvalidQueryArgumentValueError(string, + 'a valid datetime string') date_format = cls.date_formats[ordinal] try: date = datetime.strptime(string, date_format) except ValueError: # Parsing failed. - raise InvalidQueryArgumentTypeError(string, - 'a valid datetime string') + raise InvalidQueryArgumentValueError(string, + 'a valid datetime string') precision = cls.precisions[ordinal] return cls(date, precision) @@ -686,7 +686,7 @@ class DurationQuery(NumericQuery): try: return float(s) except ValueError: - raise InvalidQueryArgumentTypeError( + raise InvalidQueryArgumentValueError( s, u"a M:SS string or a float") diff --git a/beets/library.py b/beets/library.py index b263ecd64..56fd8f65b 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1306,7 +1306,7 @@ class Library(dbcore.Database): query, parsed_sort = parse_query_string(query, model_cls) elif isinstance(query, (list, tuple)): query, parsed_sort = parse_query_parts(query, model_cls) - except dbcore.query.InvalidQueryArgumentTypeError as exc: + except dbcore.query.InvalidQueryArgumentValueError as exc: raise dbcore.InvalidQueryError(query, exc) # Any non-null sort specified by the parsed query overrides the diff --git a/test/test_datequery.py b/test/test_datequery.py index e81544aaa..8ca5680c6 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -22,7 +22,7 @@ from datetime import datetime import unittest import time from beets.dbcore.query import _parse_periods, DateInterval, DateQuery,\ - InvalidQueryArgumentTypeError + InvalidQueryArgumentValueError def _date(string): @@ -118,11 +118,11 @@ class DateQueryTest(_common.LibTestCase): class DateQueryConstructTest(unittest.TestCase): def test_long_numbers(self): - with self.assertRaises(InvalidQueryArgumentTypeError): + with self.assertRaises(InvalidQueryArgumentValueError): DateQuery('added', '1409830085..1412422089') def test_too_many_components(self): - with self.assertRaises(InvalidQueryArgumentTypeError): + with self.assertRaises(InvalidQueryArgumentValueError): DateQuery('added', '12-34-56-78') def test_invalid_date_query(self): @@ -137,7 +137,7 @@ class DateQueryConstructTest(unittest.TestCase): '..2aa' ] for q in q_list: - with self.assertRaises(InvalidQueryArgumentTypeError): + with self.assertRaises(InvalidQueryArgumentValueError): DateQuery('added', q) diff --git a/test/test_query.py b/test/test_query.py index 3538c15a8..61df3ca10 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -30,7 +30,7 @@ import beets.library from beets import dbcore from beets.dbcore import types from beets.dbcore.query import (NoneQuery, ParsingError, - InvalidQueryArgumentTypeError) + InvalidQueryArgumentValueError) from beets.library import Library, Item from beets import util import platform @@ -301,11 +301,11 @@ class GetTest(DummyDataTestCase): self.assertFalse(results) def test_invalid_query(self): - with self.assertRaises(InvalidQueryArgumentTypeError) as raised: + with self.assertRaises(InvalidQueryArgumentValueError) as raised: dbcore.query.NumericQuery('year', u'199a') self.assertIn(u'not an int', six.text_type(raised.exception)) - with self.assertRaises(InvalidQueryArgumentTypeError) as raised: + with self.assertRaises(InvalidQueryArgumentValueError) as raised: dbcore.query.RegexpQuery('year', u'199(') exception_text = six.text_type(raised.exception) self.assertIn(u'not a regular expression', exception_text) From 63cd799e8d4e7ecb43878562d4ff2007931b6282 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Sat, 22 Apr 2017 00:56:52 +0100 Subject: [PATCH 02/52] Raise the correct error type The incorrect error type was reintroduced in the previous merge commit. --- beets/dbcore/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index c1150c9c4..558a434bc 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -562,7 +562,7 @@ class Period(object): # Parsing failed. pass if date is None: - raise InvalidQueryArgumentTypeError(string, + raise InvalidQueryArgumentValueError(string, 'a valid datetime string') precision = cls.precisions[ordinal] return cls(date, precision) From d24b373c8715d54e12b54334ce062a4b2bc44287 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Sat, 22 Apr 2017 01:11:48 +0100 Subject: [PATCH 03/52] Adjust indentation to pass flake8 tests --- beets/dbcore/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 558a434bc..e532ed419 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -563,7 +563,7 @@ class Period(object): pass if date is None: raise InvalidQueryArgumentValueError(string, - 'a valid datetime string') + 'a valid datetime string') precision = cls.precisions[ordinal] return cls(date, precision) From c51ecd46e3b7b97ea87d88ed6c041683cbc14eda Mon Sep 17 00:00:00 2001 From: dosoe Date: Fri, 28 Apr 2017 12:10:02 +0200 Subject: [PATCH 04/52] add composer_sort tag# --- beets/autotag/__init__.py | 6 +++++ beets/autotag/hooks.py | 6 +++-- beets/autotag/mb.py | 4 ++++ beets/library.py | 1 + beets/mediafile.py | 16 +++++++++++++ test/_common.py | 1 + test/test_mediafile.py | 48 ++++++++++++++++++++------------------- 7 files changed, 57 insertions(+), 25 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 822bb60ef..a820535cb 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -49,8 +49,12 @@ def apply_item_metadata(item, track_info): 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 + + # At the moment, the other metadata is left intact (including album # and track number). Perhaps these should be emptied? @@ -155,6 +159,8 @@ def apply_metadata(album_info, mapping): 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 diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 3c403fcf4..0c3f78ad3 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -144,6 +144,7 @@ class TrackInfo(object): - ``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.) @@ -155,8 +156,8 @@ class TrackInfo(object): 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, arranger=None, - track_alt=None): + media=None, lyricist=None, composer=None, composer_sort=None, + arranger=None, track_alt=None): self.title = title self.track_id = track_id self.artist = artist @@ -174,6 +175,7 @@ class TrackInfo(object): self.data_url = data_url self.lyricist = lyricist self.composer = composer + self.composer_sort = composer_sort self.arranger = arranger self.track_alt = track_alt diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 21dd8a715..e0c217513 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -207,6 +207,7 @@ def track_info(recording, index=None, medium=None, medium_index=None, lyricist = [] composer = [] + composer_sort = [] for work_relation in recording.get('work-relation-list', ()): if work_relation['type'] != 'performance': continue @@ -218,12 +219,15 @@ def track_info(recording, index=None, medium=None, medium_index=None, lyricist.append(artist_relation['artist']['name']) elif type == 'composer': composer.append(artist_relation['artist']['name']) + composer_sort.append(artist_relation['artist']['sort-name']) if lyricist: info.lyricist = u', '.join(lyricist) if composer: info.composer = u', '.join(composer) + info.composer_sort = u', '.join(composer_sort) arranger = [] + arranger_sort = [] for artist_relation in recording.get('artist-relation-list', ()): if 'type' in artist_relation: type = artist_relation['type'] diff --git a/beets/library.py b/beets/library.py index b263ecd64..094a85d6b 100644 --- a/beets/library.py +++ b/beets/library.py @@ -417,6 +417,7 @@ class Item(LibModel): 'genre': types.STRING, 'lyricist': types.STRING, 'composer': types.STRING, + 'composer_sort': types.STRING, 'arranger': types.STRING, 'grouping': types.STRING, 'year': types.PaddedInt(4), diff --git a/beets/mediafile.py b/beets/mediafile.py index 13f1b2dfb..fd94587a8 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1632,12 +1632,28 @@ class MediaFile(object): StorageStyle('LYRICIST'), ASFStorageStyle('WM/Writer'), ) + composer = MediaField( MP3StorageStyle('TCOM'), MP4StorageStyle('\xa9wrt'), StorageStyle('COMPOSER'), ASFStorageStyle('WM/Composer'), ) + + composer_sort = MediaField( + MP3StorageStyle('TSOC'), + MP4StorageStyle('soco'), + StorageStyle('COMPOSERSORT'), + ASFStorageStyle('WM/Composersort'), + ) + + arranger = MediaField( + MP3PeopleStorageStyle('TIPL', involvement='arranger'), + MP4StorageStyle('----:com.apple.iTunes:Arranger'), + StorageStyle('ARRANGER'), + ASFStorageStyle('beets/Arranger'), + ) + arranger = MediaField( MP3PeopleStorageStyle('TIPL', involvement='arranger'), MP4StorageStyle('----:com.apple.iTunes:Arranger'), diff --git a/test/_common.py b/test/_common.py index f3213ec31..221903f68 100644 --- a/test/_common.py +++ b/test/_common.py @@ -68,6 +68,7 @@ def item(lib=None): genre=u'the genre', lyricist=u'the lyricist', composer=u'the composer', + composer_sort=u'the sortname of the composer', arranger=u'the arranger', grouping=u'the grouping', year=1, diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 63df38b8e..f11e060df 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -318,29 +318,30 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, """ full_initial_tags = { - 'title': u'full', - 'artist': u'the artist', - 'album': u'the album', - 'genre': u'the genre', - 'composer': u'the composer', - 'grouping': u'the grouping', - 'year': 2001, - 'month': None, - 'day': None, - 'date': datetime.date(2001, 1, 1), - 'track': 2, - 'tracktotal': 3, - 'disc': 4, - 'disctotal': 5, - 'lyrics': u'the lyrics', - 'comments': u'the comments', - 'bpm': 6, - 'comp': True, - 'mb_trackid': '8b882575-08a5-4452-a7a7-cbb8a1531f9e', - 'mb_albumid': '9e873859-8aa4-4790-b985-5a953e8ef628', - 'mb_artistid': '7cf0ea9d-86b9-4dad-ba9e-2355a64899ea', - 'art': None, - 'label': u'the label', + 'title': u'full', + 'artist': u'the artist', + 'album': u'the album', + 'genre': u'the genre', + 'composer': u'the composer', + 'composer_sort' :u'the sortname of the composer', + 'grouping': u'the grouping', + 'year': 2001, + 'month': None, + 'day': None, + 'date': datetime.date(2001, 1, 1), + 'track': 2, + 'tracktotal': 3, + 'disc': 4, + 'disctotal': 5, + 'lyrics': u'the lyrics', + 'comments': u'the comments', + 'bpm': 6, + 'comp': True, + 'mb_trackid': '8b882575-08a5-4452-a7a7-cbb8a1531f9e', + 'mb_albumid': '9e873859-8aa4-4790-b985-5a953e8ef628', + 'mb_artistid': '7cf0ea9d-86b9-4dad-ba9e-2355a64899ea', + 'art': None, + 'label': u'the label', } tag_fields = [ @@ -350,6 +351,7 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, 'genre', 'lyricist', 'composer', + 'composer_sort', 'arranger', 'grouping', 'year', From e3c37981bba0ff3ee8e697188bedd706a4e091e3 Mon Sep 17 00:00:00 2001 From: dosoe Date: Fri, 28 Apr 2017 12:13:27 +0200 Subject: [PATCH 05/52] little indentation stuff --- test/test_mediafile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index f11e060df..aa7d6341e 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -323,7 +323,7 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, 'album': u'the album', 'genre': u'the genre', 'composer': u'the composer', - 'composer_sort' :u'the sortname of the composer', + 'composer_sort': u'the sortname of the composer', 'grouping': u'the grouping', 'year': 2001, 'month': None, From 075e2432bfd5b7bbd386b642f128c318054c821a Mon Sep 17 00:00:00 2001 From: dosoe Date: Fri, 28 Apr 2017 12:15:37 +0200 Subject: [PATCH 06/52] deleted one duplicate block --- beets/autotag/mb.py | 1 - beets/mediafile.py | 7 ------- 2 files changed, 8 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index e0c217513..c2172d0ac 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -227,7 +227,6 @@ def track_info(recording, index=None, medium=None, medium_index=None, info.composer_sort = u', '.join(composer_sort) arranger = [] - arranger_sort = [] for artist_relation in recording.get('artist-relation-list', ()): if 'type' in artist_relation: type = artist_relation['type'] diff --git a/beets/mediafile.py b/beets/mediafile.py index fd94587a8..c04cdb67b 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1647,13 +1647,6 @@ class MediaFile(object): ASFStorageStyle('WM/Composersort'), ) - arranger = MediaField( - MP3PeopleStorageStyle('TIPL', involvement='arranger'), - MP4StorageStyle('----:com.apple.iTunes:Arranger'), - StorageStyle('ARRANGER'), - ASFStorageStyle('beets/Arranger'), - ) - arranger = MediaField( MP3PeopleStorageStyle('TIPL', involvement='arranger'), MP4StorageStyle('----:com.apple.iTunes:Arranger'), From 2a418a6350c9266181c715fd0f9dd8077f9fd1c0 Mon Sep 17 00:00:00 2001 From: dosoe Date: Fri, 28 Apr 2017 12:29:44 +0200 Subject: [PATCH 07/52] ASFStorageStyle corrected --- beets/mediafile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index c04cdb67b..e5e7af3ec 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1644,7 +1644,7 @@ class MediaFile(object): MP3StorageStyle('TSOC'), MP4StorageStyle('soco'), StorageStyle('COMPOSERSORT'), - ASFStorageStyle('WM/Composersort'), + ASFStorageStyle('WM/Composersortorder'), ) arranger = MediaField( From d4ff82e46fef897dde4aee6ebe8781f563a6f39c Mon Sep 17 00:00:00 2001 From: dosoe Date: Fri, 28 Apr 2017 12:45:31 +0200 Subject: [PATCH 08/52] adding image stuff for composer_sort --- test/test_mediafile.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index aa7d6341e..179999dc6 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -141,6 +141,12 @@ class ImageStructureTestMixin(ArtTestMixin): type=ImageType.composer) mediafile.images += [image] mediafile.save() + + image = Image(data=self.png_data, desc=u'the sortname of the composer', + type=ImageType.composer_sort) + mediafile.images += [image] + mediafile.save() + mediafile = MediaFile(mediafile.path) self.assertEqual(len(mediafile.images), 3) @@ -150,6 +156,12 @@ class ImageStructureTestMixin(ArtTestMixin): self.assertExtendedImageAttributes( image, desc=u'the composer', type=ImageType.composer ) + + images = (i for i in mediafile.images if i.desc == u'the sortname of the composer') + image = next(images, None) + self.assertExtendedImageAttributes( + image, desc=u'the sortname of the composer', type=ImageType.composer_sort + ) def test_delete_image_structures(self): mediafile = self._mediafile_fixture('image') @@ -193,6 +205,11 @@ class ExtendedImageStructureTestMixin(ImageStructureTestMixin): type=ImageType.composer) mediafile.images += [image] mediafile.save() + + image = Image(data=self.tiff_data, desc=u'the sortname of the composer', + type=ImageType.composer_sort) + mediafile.images += [image] + mediafile.save() mediafile = MediaFile(mediafile.path) self.assertEqual(len(mediafile.images), 3) @@ -202,6 +219,11 @@ class ExtendedImageStructureTestMixin(ImageStructureTestMixin): mediafile.images))[0] self.assertExtendedImageAttributes( image, desc=u'the composer', type=ImageType.composer) + + image = list(filter(lambda i: i.mime_type == 'image/tiff', + mediafile.images))[0] + self.assertExtendedImageAttributes( + image, desc=u'the sortname of the composer', type=ImageType.composer_sort) class LazySaveTestMixin(object): From 4a17da8e10c64ad889bfa42a0941ae2c95c29443 Mon Sep 17 00:00:00 2001 From: dosoe Date: Fri, 28 Apr 2017 13:41:54 +0200 Subject: [PATCH 09/52] requested changes: where there is no artist_sort, there is no need for composer_sort; cleaning up whitespaces. --- beets/autotag/__init__.py | 2 -- beets/autotag/hooks.py | 2 +- beets/mediafile.py | 4 ---- test/test_mediafile.py | 24 ------------------------ 4 files changed, 1 insertion(+), 31 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index a820535cb..4c5f09eb4 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -53,8 +53,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 - - # At the moment, the other metadata is left intact (including album # and track number). Perhaps these should be emptied? diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 0c3f78ad3..053d050c6 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -156,7 +156,7 @@ class TrackInfo(object): 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, + media=None, lyricist=None, composer=None, composer_sort=None, arranger=None, track_alt=None): self.title = title self.track_id = track_id diff --git a/beets/mediafile.py b/beets/mediafile.py index e5e7af3ec..9f37b70d5 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1632,28 +1632,24 @@ class MediaFile(object): StorageStyle('LYRICIST'), ASFStorageStyle('WM/Writer'), ) - composer = MediaField( MP3StorageStyle('TCOM'), MP4StorageStyle('\xa9wrt'), StorageStyle('COMPOSER'), ASFStorageStyle('WM/Composer'), ) - composer_sort = MediaField( MP3StorageStyle('TSOC'), MP4StorageStyle('soco'), StorageStyle('COMPOSERSORT'), ASFStorageStyle('WM/Composersortorder'), ) - arranger = MediaField( MP3PeopleStorageStyle('TIPL', involvement='arranger'), MP4StorageStyle('----:com.apple.iTunes:Arranger'), StorageStyle('ARRANGER'), ASFStorageStyle('beets/Arranger'), ) - grouping = MediaField( MP3StorageStyle('TIT1'), MP4StorageStyle('\xa9grp'), diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 179999dc6..961d3d3bd 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -141,12 +141,6 @@ class ImageStructureTestMixin(ArtTestMixin): type=ImageType.composer) mediafile.images += [image] mediafile.save() - - image = Image(data=self.png_data, desc=u'the sortname of the composer', - type=ImageType.composer_sort) - mediafile.images += [image] - mediafile.save() - mediafile = MediaFile(mediafile.path) self.assertEqual(len(mediafile.images), 3) @@ -156,12 +150,6 @@ class ImageStructureTestMixin(ArtTestMixin): self.assertExtendedImageAttributes( image, desc=u'the composer', type=ImageType.composer ) - - images = (i for i in mediafile.images if i.desc == u'the sortname of the composer') - image = next(images, None) - self.assertExtendedImageAttributes( - image, desc=u'the sortname of the composer', type=ImageType.composer_sort - ) def test_delete_image_structures(self): mediafile = self._mediafile_fixture('image') @@ -205,11 +193,6 @@ class ExtendedImageStructureTestMixin(ImageStructureTestMixin): type=ImageType.composer) mediafile.images += [image] mediafile.save() - - image = Image(data=self.tiff_data, desc=u'the sortname of the composer', - type=ImageType.composer_sort) - mediafile.images += [image] - mediafile.save() mediafile = MediaFile(mediafile.path) self.assertEqual(len(mediafile.images), 3) @@ -219,11 +202,6 @@ class ExtendedImageStructureTestMixin(ImageStructureTestMixin): mediafile.images))[0] self.assertExtendedImageAttributes( image, desc=u'the composer', type=ImageType.composer) - - image = list(filter(lambda i: i.mime_type == 'image/tiff', - mediafile.images))[0] - self.assertExtendedImageAttributes( - image, desc=u'the sortname of the composer', type=ImageType.composer_sort) class LazySaveTestMixin(object): @@ -345,7 +323,6 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, 'album': u'the album', 'genre': u'the genre', 'composer': u'the composer', - 'composer_sort': u'the sortname of the composer', 'grouping': u'the grouping', 'year': 2001, 'month': None, @@ -373,7 +350,6 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, 'genre', 'lyricist', 'composer', - 'composer_sort', 'arranger', 'grouping', 'year', From 23f172d03d838d1119df09927e14d9f13cc14e59 Mon Sep 17 00:00:00 2001 From: dosoe Date: Fri, 28 Apr 2017 13:49:50 +0200 Subject: [PATCH 10/52] if there is no artist_sort, there should not be a composer_sort. --- test/_common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/_common.py b/test/_common.py index 221903f68..f3213ec31 100644 --- a/test/_common.py +++ b/test/_common.py @@ -68,7 +68,6 @@ def item(lib=None): genre=u'the genre', lyricist=u'the lyricist', composer=u'the composer', - composer_sort=u'the sortname of the composer', arranger=u'the arranger', grouping=u'the grouping', year=1, From ac06be1a5a6c584c18d4e6695657d1ac84b05651 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Tue, 26 Apr 2016 22:32:31 +0100 Subject: [PATCH 11/52] Add flake8 check for blind excepts Add flake8 check for blind excepts using flake8-blind-except (B901). --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 43bff8014..8c3731f5d 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,7 @@ deps = flake8 flake8-coding flake8-future-import + flake8-blind-except pep8-naming files = beets beetsplug beet test setup.py docs From bb5629ea1d3664594855064e15255b45faa6b13d Mon Sep 17 00:00:00 2001 From: wordofglass Date: Tue, 26 Apr 2016 00:14:35 +0200 Subject: [PATCH 12/52] Remove untyped except statements --- beets/mediafile.py | 2 +- beets/plugins.py | 2 +- beets/util/functemplate.py | 2 +- beets/util/pipeline.py | 8 ++++---- beetsplug/duplicates.py | 2 +- beetsplug/thumbnails.py | 4 ---- 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 13f1b2dfb..bd9b11969 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1317,7 +1317,7 @@ class DateField(MediaField): for item in items: try: items_.append(int(item)) - except: + except (TypeError, ValueError): items_.append(None) return items_ diff --git a/beets/plugins.py b/beets/plugins.py index 2ecdb8472..d62f3c011 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -264,7 +264,7 @@ def load_plugins(names=()): and obj != BeetsPlugin and obj not in _classes: _classes.add(obj) - except: + except Exception: log.warning( u'** error loading plugin {}:\n{}', name, diff --git a/beets/util/functemplate.py b/beets/util/functemplate.py index 51716552c..58b0416a1 100644 --- a/beets/util/functemplate.py +++ b/beets/util/functemplate.py @@ -570,7 +570,7 @@ class Template(object): """ try: res = self.compiled(values, functions) - except: # Handle any exceptions thrown by compiled version. + except Exception: # Handle any exceptions thrown by compiled version. res = self.interpret(values, functions) return res diff --git a/beets/util/pipeline.py b/beets/util/pipeline.py index 367e5d980..39bc7152e 100644 --- a/beets/util/pipeline.py +++ b/beets/util/pipeline.py @@ -270,7 +270,7 @@ class FirstPipelineThread(PipelineThread): return self.out_queue.put(msg) - except: + except BaseException: self.abort_all(sys.exc_info()) return @@ -318,7 +318,7 @@ class MiddlePipelineThread(PipelineThread): return self.out_queue.put(msg) - except: + except BaseException: self.abort_all(sys.exc_info()) return @@ -357,7 +357,7 @@ class LastPipelineThread(PipelineThread): # Send to consumer. self.coro.send(msg) - except: + except BaseException: self.abort_all(sys.exc_info()) return @@ -425,7 +425,7 @@ class Pipeline(object): while threads[-1].is_alive(): threads[-1].join(1) - except: + except BaseException: # Stop all the threads immediately. for thread in threads: thread.abort() diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 93d53c58a..2f6bba3e6 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -185,7 +185,7 @@ class DuplicatesPlugin(BeetsPlugin): if tag: try: k, v = tag.split('=') - except: + except Exception: raise UserError( u"{}: can't parse k=v tag: {}".format(PLUGIN, tag) ) diff --git a/beetsplug/thumbnails.py b/beetsplug/thumbnails.py index 838206156..04845e880 100644 --- a/beetsplug/thumbnails.py +++ b/beetsplug/thumbnails.py @@ -274,8 +274,6 @@ class GioURI(URIGetter): try: uri_ptr = self.libgio.g_file_get_uri(g_file_ptr) - except: - raise finally: self.libgio.g_object_unref(g_file_ptr) if not uri_ptr: @@ -285,8 +283,6 @@ class GioURI(URIGetter): try: uri = copy_c_string(uri_ptr) - except: - raise finally: self.libgio.g_free(uri_ptr) From 287e077709c4ef20a6d02126c2a0a13bb908b2b7 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Thu, 28 Apr 2016 19:27:02 +0200 Subject: [PATCH 13/52] Remove blind excepts from bluelet.py --- beets/util/bluelet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/util/bluelet.py b/beets/util/bluelet.py index 48dd7bd94..0da17559b 100644 --- a/beets/util/bluelet.py +++ b/beets/util/bluelet.py @@ -269,7 +269,7 @@ def run(root_coro): except StopIteration: # Thread is done. complete_thread(coro, None) - except: + except BaseException: # Thread raised some other exception. del threads[coro] raise ThreadException(coro, sys.exc_info()) @@ -366,7 +366,7 @@ def run(root_coro): exit_te = te break - except: + except BaseException: # For instance, KeyboardInterrupt during select(). Raise # into root thread and terminate others. threads = {root_coro: ExceptionEvent(sys.exc_info())} From f622e42a88c7a5b9c92d29a2e0a587b335462839 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Thu, 28 Apr 2016 23:42:40 +0100 Subject: [PATCH 14/52] Replace blind excepts with generic Exception excepts in tests --- test/test_logging.py | 2 +- test/test_mediafile.py | 2 +- test/test_replaygain.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_logging.py b/test/test_logging.py index a6b02a572..826b2447b 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -257,7 +257,7 @@ class ConcurrentEventsTest(TestCase, helper.TestHelper): t2.join(.1) self.assertFalse(t2.is_alive()) - except: + except Exception: print(u"Alive threads:", threading.enumerate()) if dp.lock1.locked(): print(u"Releasing lock1 after exception in test") diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 63df38b8e..820bdb412 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -913,7 +913,7 @@ class AIFFTest(ReadWriteTestBase, unittest.TestCase): # remove this once we require a version that includes the feature. try: import mutagen.dsf # noqa -except: +except ImportError: HAVE_DSF = False else: HAVE_DSF = True diff --git a/test/test_replaygain.py b/test/test_replaygain.py index 6ea21ecb6..6ddee54da 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -52,14 +52,14 @@ class ReplayGainCliTestBase(TestHelper): try: self.load_plugins('replaygain') - except: + except Exception: import sys # store exception info so an error in teardown does not swallow it exc_info = sys.exc_info() try: self.teardown_beets() self.unload_plugins() - except: + except Exception: # if load_plugins() failed then setup is incomplete and # teardown operations may fail. In particular # {Item,Album} # may not have the _original_types attribute in unload_plugins From 813b078d026f37268e89cd6bb782a2a42e7d32e3 Mon Sep 17 00:00:00 2001 From: dosoe Date: Sat, 29 Apr 2017 18:47:03 +0200 Subject: [PATCH 15/52] added composer_sort on test_mediafile.py since there is artist_sort --- test/test_mediafile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 961d3d3bd..580584791 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -350,6 +350,7 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, 'genre', 'lyricist', 'composer', + 'composer_sort', 'arranger', 'grouping', 'year', From e9c3d69e5991b8f45c6602dc6e0584522b7b3961 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 29 Apr 2017 18:29:36 -0400 Subject: [PATCH 16/52] Fix a typo --- beets/ui/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 06ab6f0a5..90896a357 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -84,7 +84,7 @@ def _do_query(lib, query, album, also_items=True): def _print_keys(query): """Given a SQLite query result, print the `key` field of each - returned row, with identation of 2 spaces. + returned row, with indentation of 2 spaces. """ for row in query: print_(u' ' * 2 + row['key']) From dd7b129e217ab39717f662c43fc7a8ea347ffb8c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 29 Apr 2017 18:29:47 -0400 Subject: [PATCH 17/52] Turn off unnecessary execute bit --- beets/ui/commands.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 beets/ui/commands.py diff --git a/beets/ui/commands.py b/beets/ui/commands.py old mode 100755 new mode 100644 From 8e78cfdac7cbb4f5bc6a23a40c11295783e88f71 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 29 Apr 2017 21:13:34 -0400 Subject: [PATCH 18/52] Always pass unicode to print_ Introduced in #2495. --- beets/ui/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 90896a357..995ff87e9 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -615,7 +615,7 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None, require = True # Bell ring when user interaction is needed. if config['import']['bell']: - ui.print_('\a', end='') + ui.print_(u'\a', end=u'') sel = ui.input_options((u'Apply', u'More candidates') + choice_opts, require=require, default=default) if sel == u'a': From 2bf58a61c31ebec6b0dfc0ea212fc22f3da3614b Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Sun, 30 Apr 2017 23:14:23 +0200 Subject: [PATCH 19/52] Decode string with Unicode escape --- beetsplug/lyrics.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 6714b2fee..14ca6a4b5 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -21,6 +21,7 @@ from __future__ import absolute_import, division, print_function import difflib import itertools import json +import struct import re import requests import unicodedata @@ -77,6 +78,11 @@ USER_AGENT = 'beets/{}'.format(beets.__version__) # Utilities. +def unichar(i): + try: + return six.unichr(i) + except ValueError: + return struct.pack('i', i).decode('utf-32') def unescape(text): """Resolve &#xxx; HTML entities (and some others).""" @@ -86,7 +92,7 @@ def unescape(text): def replchar(m): num = m.group(1) - return six.unichr(int(num)) + return unichar(int(num)) out = re.sub(u"&#(\d+);", replchar, out) return out From a165d6c00bf022345852e20ccc6624fd3d5da04d Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Mon, 1 May 2017 23:40:09 +0200 Subject: [PATCH 20/52] Fix MusiXmatch text extraction markers --- beetsplug/lyrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 14ca6a4b5..ad2d278b5 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -263,8 +263,8 @@ class MusiXmatch(SymbolsReplaced): html = self.fetch_url(url) if not html: return - lyrics = extract_text_between(html, - '"body":', '"language":') + lyrics = extract_text_between(html, '

', + '

') return lyrics.strip(',"').replace('\\n', '\n') From f8862ac0ea8d3a055a5ebfc0abda0c2b28773947 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Tue, 2 May 2017 00:52:07 +0200 Subject: [PATCH 21/52] Sort imports --- test/test_lyrics.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 13ba07fdf..eb9d17dec 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -15,21 +15,25 @@ """Tests for the 'lyrics' plugin.""" -from __future__ import division, absolute_import, print_function +from __future__ import absolute_import, division, print_function import os -import sys import re +import six +import sys import unittest +from mock import patch from test import _common -from mock import MagicMock + +from beets import logging +from beets.library import Item +from beets.util import bytestring_path, confit from beetsplug import lyrics -from beets.library import Item -from beets.util import confit, bytestring_path -from beets import logging -import six + +from mock import MagicMock + log = logging.getLogger('beets.test_lyrics') raw_backend = lyrics.Backend({}, log) From 4e0527f07d5cdfa623216caa21904d74fdd289dd Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Tue, 2 May 2017 00:54:58 +0200 Subject: [PATCH 22/52] Docstrings style --- test/test_lyrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index eb9d17dec..d8b9e672e 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -42,7 +42,7 @@ google = lyrics.Google(MagicMock(), log) class LyricsPluginTest(unittest.TestCase): def setUp(self): - """Set up configuration""" + """Set up configuration.""" lyrics.LyricsPlugin() def test_search_artist(self): @@ -317,7 +317,7 @@ class LyricsGooglePluginTest(unittest.TestCase): title=u'Beets song', path=u'/lyrics/beetssong') def setUp(self): - """Set up configuration""" + """Set up configuration.""" try: __import__('bs4') except ImportError: From a85dcd88c4898c067982e7a87f7234be99aa9f5d Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Tue, 2 May 2017 00:56:56 +0200 Subject: [PATCH 23/52] Store whole expected lyrics, not just keywords, but randomized --- test/rsrc/lyricstext.yaml | 87 ++++++++++++++++++++++----------------- test/test_lyrics.py | 10 +++-- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/test/rsrc/lyricstext.yaml b/test/rsrc/lyricstext.yaml index 814c207df..7ae1a70e7 100644 --- a/test/rsrc/lyricstext.yaml +++ b/test/rsrc/lyricstext.yaml @@ -1,45 +1,56 @@ -Beets_song: - - geeks - - bouquet - - panacea +# Song used by LyricsGooglePluginMachineryTest -Amsterdam: - - oriflammes - - fortune - - batave - - pissent - -Lady_Madonna: - - heaven - - tuesday - - thursday - -Jazz_n_blues: - - parkway - - balance - - impatient - - shoes - -Hey_it_s_ok: - - swear - - forgive - - drink - - found - -City_of_dreams: - - groves - - landmarks - - twilight - - freeways - -Black_magic_woman: - - devil - - magic - - spell - - heart +Beets_song: | + beets is the media library management system for obsessive-compulsive music geeks the purpose of + beets is to get your music collection right once and for all it catalogs your collection + automatically improving its metadata as it goes it then provides a bouquet of tools for + manipulating and accessing your music here's an example of beets' brainy tag corrector doing its + because beets is designed as a library it can do almost anything you can imagine for your + music collection via plugins beets becomes a panacea missing_texts: | Lyricsmania staff is working hard for you to add $TITLE lyrics as soon as they'll be released by $ARTIST, check back soon! In case you have the lyrics to $TITLE and want to send them to us, fill out the following form. + +# Songs lyrics used to test the different sources present in the google custom search engine. +# Text is randomized for copyright infringement reason. + +Amsterdam: | + coup corps coeur invitent mains comme trop morue le hantent mais la dames joli revenir aux + mangent croquer pleine plantent rire de sortent pleins fortune d'amsterdam bruit ruisselants + large poissons braguette leur putains blanches jusque pissent dans soleils dansent et port + bien vertu nez sur chaleur femmes rotant dorment marins boivent bu les que d'un qui je + une cou hambourg plus ils dents ou tournent or berges d'ailleurs tout ciel haubans ce son lueurs + en lune ont mouchent leurs long frottant jusqu'en vous regard montrent langueurs chantent + tordent pleure donnent drames mornes des panse pour un sent encore referment nappes au meurent + geste quand puis alors frites grosses batave expire naissent reboivent oriflammes grave riant a + enfin rance fier y bouffer s'entendre se mieux + +Lady_Madonna: | + feed his money tuesday manage didn't head feet see arrives at in madonna rest morning children + wonder how make thursday your to sunday music papers come tie you has was is listen suitcase + ends friday run that needed breast they child baby mending on lady learned a nun like did wednesday + bed think without afternoon night meet the playing lying + +Jazz_n_blues: | + all shoes money through follow blow til father to his hit jazz kiss now cool bar cause 50 night + heading i'll says yeah cash forgot blues out what for ways away fingers waiting got ever bold + screen sixty throw wait on about last compton days o pick love wall had within jeans jd next + miss standing from it's two long fight extravagant tell today more buy shopping that didn't + what's but russian up can parkway balance my and gone am it as at in check if bags when cross + machine take you drinks coke june wrong coming fancy's i n' impatient so the main's spend + that's + +Hey_it_s_ok: | + and forget be when please it against fighting mama cause ! again what said + things papa hey to much lovers way wet was too do drink and i who forgive + hey fourteen please know not wanted had myself ok friends bed times looked + swear act found the my mean + +Black_magic_woman: | + blind heart sticks just don't into back alone see need yes your out devil make that to black got + you might me woman turning spell stop baby with 'round a on stone messin' magic i of + tricks up leave turn bad so pick she's my can't + diff --git a/test/test_lyrics.py b/test/test_lyrics.py index d8b9e672e..d36499d16 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -221,10 +221,12 @@ class MockFetchUrl(object): def is_lyrics_content_ok(title, text): - """Compare lyrics text to expected lyrics for given title""" - - keywords = LYRICS_TEXTS[google.slugify(title)] - return all(x in text.lower() for x in keywords) + """Compare lyrics text to expected lyrics for given title.""" + if not text: + return + keywords = set(LYRICS_TEXTS[google.slugify(title)].split()) + words = set(x.strip(".?, ") for x in text.lower().split()) + return keywords <= words LYRICS_ROOT_DIR = os.path.join(_common.RSRC, b'lyrics') LYRICS_TEXTS = confit.load_yaml(os.path.join(_common.RSRC, b'lyricstext.yaml')) From d88cabc8464ab338b143e336afcf03ace2303266 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Tue, 2 May 2017 01:03:26 +0200 Subject: [PATCH 24/52] Divide LyricsGooglePluginTest into two classes. Move existing tests into LyricsGooglePluginMachineryTest. Create LyricsPluginSourcesTest class to check fetching of each source. Some code was supposed to do that until now but was never executed as we exited early at the "if not check_lyrics_fetched():" check. --- test/test_lyrics.py | 227 +++++++++++++++++++------------------------- 1 file changed, 100 insertions(+), 127 deletions(-) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index d36499d16..b6abf8d9e 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -198,15 +198,6 @@ def url_to_filename(url): return fn -def check_lyrics_fetched(): - """Return True if lyrics_download_samples.py has been runned and lyrics - pages are present in resources directory""" - lyrics_dirs = len([d for d in os.listdir(LYRICS_ROOT_DIR) if - os.path.isdir(os.path.join(LYRICS_ROOT_DIR, d))]) - # example.com is the only lyrics dir added to repo - return lyrics_dirs > 1 - - class MockFetchUrl(object): def __init__(self, pathval='fetched_path'): self.pathval = pathval @@ -230,94 +221,9 @@ def is_lyrics_content_ok(title, text): LYRICS_ROOT_DIR = os.path.join(_common.RSRC, b'lyrics') LYRICS_TEXTS = confit.load_yaml(os.path.join(_common.RSRC, b'lyricstext.yaml')) -DEFAULT_SONG = dict(artist=u'The Beatles', title=u'Lady Madonna') - -DEFAULT_SOURCES = [ - dict(DEFAULT_SONG, url=u'http://lyrics.wikia.com/', - path=u'The_Beatles:Lady_Madonna'), - dict(artist=u'Santana', title=u'Black magic woman', - url='http://www.lyrics.com/', - path=u'black-magic-woman-lyrics-santana.html'), - dict(DEFAULT_SONG, url='https://www.musixmatch.com/', - path=u'lyrics/The-Beatles/Lady-Madonna'), -] - -# Every source entered in default beets google custom search engine -# must be listed below. -# Use default query when possible, or override artist and title fields -# if website don't have lyrics for default query. -GOOGLE_SOURCES = [ - dict(DEFAULT_SONG, - url=u'http://www.absolutelyrics.com', - path=u'/lyrics/view/the_beatles/lady_madonna'), - dict(DEFAULT_SONG, - url=u'http://www.azlyrics.com', - path=u'/lyrics/beatles/ladymadonna.html'), - dict(DEFAULT_SONG, - url=u'http://www.chartlyrics.com', - path=u'/_LsLsZ7P4EK-F-LD4dJgDQ/Lady+Madonna.aspx'), - dict(DEFAULT_SONG, - url=u'http://www.elyricsworld.com', - path=u'/lady_madonna_lyrics_beatles.html'), - dict(url=u'http://www.lacoccinelle.net', - artist=u'Jacques Brel', title=u"Amsterdam", - path=u'/paroles-officielles/275679.html'), - dict(DEFAULT_SONG, - url=u'http://letras.mus.br/', path=u'the-beatles/275/'), - dict(DEFAULT_SONG, - url='http://www.lyricsmania.com/', - path='lady_madonna_lyrics_the_beatles.html'), - dict(artist=u'Santana', title=u'Black magic woman', - url='http://www.lyrics.com/', - path=u'black-magic-woman-lyrics-santana.html'), - dict(DEFAULT_SONG, url=u'http://lyrics.wikia.com/', - path=u'The_Beatles:Lady_Madonna'), - dict(DEFAULT_SONG, - url=u'http://www.lyrics.net', path=u'/lyric/19110224'), - dict(DEFAULT_SONG, - url=u'http://www.lyricsmode.com', - path=u'/lyrics/b/beatles/lady_madonna.html'), - dict(url=u'http://www.lyricsontop.com', - artist=u'Amy Winehouse', title=u"Jazz'n'blues", - path=u'/amy-winehouse-songs/jazz-n-blues-lyrics.html'), - dict(DEFAULT_SONG, - url='http://www.metrolyrics.com/', - path='lady-madonna-lyrics-beatles.html'), - dict(url='http://www.musica.com/', path='letras.asp?letra=2738', - artist=u'Santana', title=u'Black magic woman'), - dict(DEFAULT_SONG, - url=u'http://www.onelyrics.net/', - artist=u'Ben & Ellen Harper', title=u'City of dreams', - path='ben-ellen-harper-city-of-dreams-lyrics'), - dict(url=u'http://www.paroles.net/', - artist=u'Lilly Wood & the prick', title=u"Hey it's ok", - path=u'lilly-wood-the-prick/paroles-hey-it-s-ok'), - dict(DEFAULT_SONG, - url='http://www.releaselyrics.com', - path=u'/346e/the-beatles-lady-madonna-(love-version)/'), - dict(DEFAULT_SONG, - url=u'http://www.smartlyrics.com', - path=u'/Song18148-The-Beatles-Lady-Madonna-lyrics.aspx'), - dict(DEFAULT_SONG, - url='http://www.songlyrics.com', - path=u'/the-beatles/lady-madonna-lyrics'), - dict(DEFAULT_SONG, - url=u'http://www.stlyrics.com', - path=u'/songs/r/richiehavens48961/ladymadonna2069109.html'), - dict(DEFAULT_SONG, - url=u'http://www.sweetslyrics.com', - path=u'/761696.The%20Beatles%20-%20Lady%20Madonna.html') -] -class LyricsGooglePluginTest(unittest.TestCase): - """Test scraping heuristics on a fake html page. - Or run lyrics_download_samples.py first to check that beets google - custom search engine sources are correctly scraped. - """ - source = dict(url=u'http://www.example.com', artist=u'John Doe', - title=u'Beets song', path=u'/lyrics/beetssong') - +class LyricsGoogleBaseTest(unittest.TestCase): def setUp(self): """Set up configuration.""" try: @@ -326,44 +232,112 @@ class LyricsGooglePluginTest(unittest.TestCase): 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") - lyrics.LyricsPlugin() - raw_backend.fetch_url = MockFetchUrl() - def test_mocked_source_ok(self): - """Test that lyrics of the mocked page are correctly scraped""" - url = self.source['url'] + self.source['path'] - if os.path.isfile(url_to_filename(url)): - res = lyrics.scrape_lyrics_from_html(raw_backend.fetch_url(url)) - self.assertTrue(google.is_lyrics(res), url) - self.assertTrue(is_lyrics_content_ok(self.source['title'], res), - url) + +class LyricsPluginSourcesTest(LyricsGoogleBaseTest): + """Check that beets google custom search engine sources are correctly scraped. + """ + + DEFAULT_SONG = dict(artist=u'The Beatles', title=u'Lady Madonna') + + DEFAULT_SOURCES = [ + dict(DEFAULT_SONG, backend=lyrics.LyricsWiki), + # dict(artist=u'Santana', title=u'Black magic woman', backend=lyrics.MusiXmatch), + # dict(DEFAULT_SONG, backend=lyrics.Genius), + ] + + GOOGLE_SOURCES = [ + dict(DEFAULT_SONG, + url=u'http://www.absolutelyrics.com', + path=u'/lyrics/view/the_beatles/lady_madonna'), + dict(DEFAULT_SONG, + url=u'http://www.azlyrics.com', + path=u'/lyrics/beatles/ladymadonna.html'), + dict(DEFAULT_SONG, + url=u'http://www.chartlyrics.com', + path=u'/_LsLsZ7P4EK-F-LD4dJgDQ/Lady+Madonna.aspx'), + dict(DEFAULT_SONG, + url=u'http://www.elyricsworld.com', + path=u'/lady_madonna_lyrics_beatles.html'), + dict(url=u'http://www.lacoccinelle.net', + artist=u'Jacques Brel', title=u"Amsterdam", + path=u'/paroles-officielles/275679.html'), + dict(DEFAULT_SONG, + url=u'http://letras.mus.br/', path=u'the-beatles/275/'), + dict(DEFAULT_SONG, + url='http://www.lyricsmania.com/', + path='lady_madonna_lyrics_the_beatles.html'), + dict(DEFAULT_SONG, url=u'http://lyrics.wikia.com/', + path=u'The_Beatles:Lady_Madonna'), + dict(DEFAULT_SONG, + url=u'http://www.lyricsmode.com', + path=u'/lyrics/b/beatles/lady_madonna.html'), + dict(url=u'http://www.lyricsontop.com', + artist=u'Amy Winehouse', title=u"Jazz'n'blues", + path=u'/amy-winehouse-songs/jazz-n-blues-lyrics.html'), + dict(DEFAULT_SONG, + url='http://www.metrolyrics.com/', + path='lady-madonna-lyrics-beatles.html'), + dict(url='http://www.musica.com/', path='letras.asp?letra=2738', + artist=u'Santana', title=u'Black magic woman'), + dict(url=u'http://www.paroles.net/', + artist=u'Lilly Wood & the prick', title=u"Hey it's ok", + path=u'lilly-wood-the-prick/paroles-hey-it-s-ok'), + dict(DEFAULT_SONG, + url='http://www.songlyrics.com', + path=u'/the-beatles/lady-madonna-lyrics'), + dict(DEFAULT_SONG, + url=u'http://www.sweetslyrics.com', + path=u'/761696.The%20Beatles%20-%20Lady%20Madonna.html') + ] + + def setUp(self): + LyricsGoogleBaseTest.setUp(self) + self.plugin = lyrics.LyricsPlugin() + + def test_backend_sources_ok(self): + """Test default backends with songs known to exist in respective databases. + """ + errors = [] + for s in self.DEFAULT_SOURCES: + res = s['backend'](self.plugin.config, self.plugin._log).fetch(s['artist'], s['title']) + if not is_lyrics_content_ok(s['title'], res): + errors.append(s['backend'].__name__) + self.assertFalse(errors) def test_google_sources_ok(self): """Test if lyrics present on websites registered in beets google custom search engine are correctly scraped.""" - if not check_lyrics_fetched(): - self.skipTest("Run lyrics_download_samples.py script first.") - for s in GOOGLE_SOURCES: + for s in self.GOOGLE_SOURCES: url = s['url'] + s['path'] - if os.path.isfile(url_to_filename(url)): - res = lyrics.scrape_lyrics_from_html( - raw_backend.fetch_url(url)) - self.assertTrue(google.is_lyrics(res), url) - self.assertTrue(is_lyrics_content_ok(s['title'], res), url) + res = lyrics.scrape_lyrics_from_html( + raw_backend.fetch_url(url)) + self.assertTrue(google.is_lyrics(res), url) + self.assertTrue(is_lyrics_content_ok(s['title'], res), url) - def test_default_ok(self): - """Test default engines with the default query""" - if not check_lyrics_fetched(): - self.skipTest("Run lyrics_download_samples.py script first.") - for (source, s) in zip([lyrics.LyricsWiki, - lyrics.LyricsCom, - lyrics.MusiXmatch], DEFAULT_SOURCES): - url = s['url'] + s['path'] - if os.path.isfile(url_to_filename(url)): - res = source({}, log).fetch(s['artist'], s['title']) - self.assertTrue(google.is_lyrics(res), url) - self.assertTrue(is_lyrics_content_ok(s['title'], res), url) +class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): + """Test scraping heuristics on a fake html page. + """ + source = dict(url=u'http://www.example.com', artist=u'John Doe', + title=u'Beets song', path=u'/lyrics/beetssong') + + def setUp(self): + """Set up configuration""" + LyricsGoogleBaseTest.setUp(self) + self.plugin = lyrics.LyricsPlugin() + + + @patch.object(lyrics.Backend, 'fetch_url', MockFetchUrl()) + def test_mocked_source_ok(self): + """Test that lyrics of the mocked page are correctly scraped""" + url = self.source['url'] + self.source['path'] + res = lyrics.scrape_lyrics_from_html(raw_backend.fetch_url(url)) + self.assertTrue(google.is_lyrics(res), url) + self.assertTrue(is_lyrics_content_ok(self.source['title'], res), + url) + + @patch.object(lyrics.Backend, 'fetch_url', MockFetchUrl()) def test_is_page_candidate_exact_match(self): """Test matching html page title with song infos -- when song infos are present in the title.""" @@ -373,8 +347,7 @@ class LyricsGooglePluginTest(unittest.TestCase): html = raw_backend.fetch_url(url) soup = BeautifulSoup(html, "html.parser", parse_only=SoupStrainer('title')) - self.assertEqual(google.is_page_candidate(url, soup.title.string, - s['title'], s['artist']), + self.assertEqual(google.is_page_candidate(url, soup.title.string, s['title'], s['artist']), True, url) def test_is_page_candidate_fuzzy_match(self): From fa9262d61b65b5023b3be6bd68bd04c434cb3b5c Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Tue, 2 May 2017 01:05:18 +0200 Subject: [PATCH 25/52] Disable tests that do real requests to lyrics sites by default. Set BEETS_TEST_LYRICS_SOURCES environment variable to '1' to not skip the tests. --- test/test_lyrics.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index b6abf8d9e..9ed0eb4b4 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -295,6 +295,8 @@ class LyricsPluginSourcesTest(LyricsGoogleBaseTest): LyricsGoogleBaseTest.setUp(self) self.plugin = lyrics.LyricsPlugin() + @unittest.skipUnless(os.environ.get('BEETS_TEST_LYRICS_SOURCES', '0') == '1', + 'lyrics sources testing not enabled') def test_backend_sources_ok(self): """Test default backends with songs known to exist in respective databases. """ @@ -305,6 +307,8 @@ class LyricsPluginSourcesTest(LyricsGoogleBaseTest): errors.append(s['backend'].__name__) self.assertFalse(errors) + @unittest.skipUnless(os.environ.get('BEETS_TEST_LYRICS_SOURCES', '0') == '1', + 'lyrics sources testing not enabled') def test_google_sources_ok(self): """Test if lyrics present on websites registered in beets google custom search engine are correctly scraped.""" From 3e3ad6974cc58d4b9b2f55f83bd01403339eb65a Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Tue, 2 May 2017 07:30:40 +0200 Subject: [PATCH 26/52] Fix PEP8 --- beetsplug/lyrics.py | 13 +++++++++---- test/test_lyrics.py | 11 +++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index ad2d278b5..9a60df119 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -84,6 +84,7 @@ def unichar(i): except ValueError: return struct.pack('i', i).decode('utf-32') + def unescape(text): """Resolve &#xxx; HTML entities (and some others).""" if isinstance(text, bytes): @@ -110,7 +111,6 @@ def extract_text_in(html, starttag): """Extract the text from a
tag in the HTML starting with ``starttag``. Returns None if parsing fails. """ - # Strip off the leading text before opening tag. try: _, html = html.split(starttag, 1) @@ -151,10 +151,10 @@ def search_pairs(item): and featured artists from the strings and add them as candidates. The method also tries to split multiple titles separated with `/`. """ - def generate_alternatives(string, patterns): """Generate string alternatives by extracting first matching group for - each given pattern.""" + each given pattern. + """ alternatives = [string] for pattern in patterns: match = re.search(pattern, string, re.IGNORECASE) @@ -270,6 +270,7 @@ class MusiXmatch(SymbolsReplaced): class Genius(Backend): """Fetch lyrics from Genius via genius-api.""" + def __init__(self, config, log): super(Genius, self).__init__(config, log) self.api_key = config['genius_api_key'].as_str() @@ -361,6 +362,7 @@ class Genius(Backend): class LyricsWiki(SymbolsReplaced): """Fetch lyrics from LyricsWiki.""" + URL_PATTERN = 'http://lyrics.wikia.com/%s:%s' def fetch(self, artist, title): @@ -381,6 +383,7 @@ class LyricsWiki(SymbolsReplaced): class LyricsCom(Backend): """Fetch lyrics from Lyrics.com.""" + URL_PATTERN = 'http://www.lyrics.com/%s-lyrics-%s.html' NOT_FOUND = ( 'Sorry, we do not have the lyric', @@ -484,6 +487,7 @@ def scrape_lyrics_from_html(html): class Google(Backend): """Fetch lyrics from Google search results.""" + def __init__(self, config, log): super(Google, self).__init__(config, log) self.api_key = config['google_API_key'].as_str() @@ -719,7 +723,8 @@ class LyricsPlugin(plugins.BeetsPlugin): def fetch_item_lyrics(self, lib, item, write, force): """Fetch and store lyrics for a single item. If ``write``, then the - lyrics will also be written to the file itself.""" + lyrics will also be written to the file itself. + """ # Skip if the item already has lyrics. if not force and item.lyrics: self._log.info(u'lyrics already present: {0}', item) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 9ed0eb4b4..0dbf658fe 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -311,7 +311,8 @@ class LyricsPluginSourcesTest(LyricsGoogleBaseTest): 'lyrics sources testing not enabled') def test_google_sources_ok(self): """Test if lyrics present on websites registered in beets google custom - search engine are correctly scraped.""" + search engine are correctly scraped. + """ for s in self.GOOGLE_SOURCES: url = s['url'] + s['path'] res = lyrics.scrape_lyrics_from_html( @@ -323,6 +324,7 @@ class LyricsPluginSourcesTest(LyricsGoogleBaseTest): class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): """Test scraping heuristics on a fake html page. """ + source = dict(url=u'http://www.example.com', artist=u'John Doe', title=u'Beets song', path=u'/lyrics/beetssong') @@ -330,7 +332,6 @@ class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): """Set up configuration""" LyricsGoogleBaseTest.setUp(self) self.plugin = lyrics.LyricsPlugin() - @patch.object(lyrics.Backend, 'fetch_url', MockFetchUrl()) def test_mocked_source_ok(self): @@ -344,7 +345,8 @@ class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): @patch.object(lyrics.Backend, 'fetch_url', MockFetchUrl()) def test_is_page_candidate_exact_match(self): """Test matching html page title with song infos -- when song infos are - present in the title.""" + present in the title. + """ from bs4 import SoupStrainer, BeautifulSoup s = self.source url = six.text_type(s['url'] + s['path']) @@ -356,7 +358,8 @@ class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): def test_is_page_candidate_fuzzy_match(self): """Test matching html page title with song infos -- when song infos are - not present in the title.""" + not present in the title. + """ s = self.source url = s['url'] + s['path'] url_title = u'example.com | Beats song by John doe' From 11eb90c7588f1d4ef688af1396f01b114f7ba833 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Tue, 2 May 2017 07:46:36 +0200 Subject: [PATCH 27/52] Fix PEP8 --- beetsplug/lyrics.py | 3 ++- test/test_lyrics.py | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 9a60df119..5cf93471c 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -263,7 +263,8 @@ class MusiXmatch(SymbolsReplaced): html = self.fetch_url(url) if not html: return - lyrics = extract_text_between(html, '

', + lyrics = extract_text_between(html, + '

', '

') return lyrics.strip(',"').replace('\\n', '\n') diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 0dbf658fe..42969e3ea 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -235,14 +235,16 @@ class LyricsGoogleBaseTest(unittest.TestCase): class LyricsPluginSourcesTest(LyricsGoogleBaseTest): - """Check that beets google custom search engine sources are correctly scraped. + """Check that beets google custom search engine sources are correctly + scraped. """ DEFAULT_SONG = dict(artist=u'The Beatles', title=u'Lady Madonna') DEFAULT_SOURCES = [ dict(DEFAULT_SONG, backend=lyrics.LyricsWiki), - # dict(artist=u'Santana', title=u'Black magic woman', backend=lyrics.MusiXmatch), + # dict(artist=u'Santana', title=u'Black magic woman', + # backend=lyrics.MusiXmatch), # dict(DEFAULT_SONG, backend=lyrics.Genius), ] @@ -295,19 +297,22 @@ class LyricsPluginSourcesTest(LyricsGoogleBaseTest): LyricsGoogleBaseTest.setUp(self) self.plugin = lyrics.LyricsPlugin() - @unittest.skipUnless(os.environ.get('BEETS_TEST_LYRICS_SOURCES', '0') == '1', + @unittest.skipUnless(os.environ.get( + 'BEETS_TEST_LYRICS_SOURCES', '0') == '1', 'lyrics sources testing not enabled') def test_backend_sources_ok(self): """Test default backends with songs known to exist in respective databases. """ errors = [] for s in self.DEFAULT_SOURCES: - res = s['backend'](self.plugin.config, self.plugin._log).fetch(s['artist'], s['title']) + res = s['backend'](self.plugin.config, self.plugin._log).fetch( + s['artist'], s['title']) if not is_lyrics_content_ok(s['title'], res): errors.append(s['backend'].__name__) self.assertFalse(errors) - @unittest.skipUnless(os.environ.get('BEETS_TEST_LYRICS_SOURCES', '0') == '1', + @unittest.skipUnless(os.environ.get( + 'BEETS_TEST_LYRICS_SOURCES', '0') == '1', 'lyrics sources testing not enabled') def test_google_sources_ok(self): """Test if lyrics present on websites registered in beets google custom @@ -353,8 +358,8 @@ class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): html = raw_backend.fetch_url(url) soup = BeautifulSoup(html, "html.parser", parse_only=SoupStrainer('title')) - self.assertEqual(google.is_page_candidate(url, soup.title.string, s['title'], s['artist']), - True, url) + self.assertEqual(google.is_page_candidate(url, soup.title.string, + s['title'], s['artist']), True, url) def test_is_page_candidate_fuzzy_match(self): """Test matching html page title with song infos -- when song infos are From 3e38a33c4a2730d83a5c9d43cdc5c9de6644b552 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Tue, 2 May 2017 23:37:20 +0200 Subject: [PATCH 28/52] Fix PEP8 --- beetsplug/lyrics.py | 2 +- test/test_lyrics.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 5cf93471c..f75b157c7 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -52,7 +52,6 @@ except ImportError: from beets import plugins from beets import ui -import beets DIV_RE = re.compile(r'<(/?)div>?', re.I) @@ -260,6 +259,7 @@ class MusiXmatch(SymbolsReplaced): def fetch(self, artist, title): url = self.build_url(artist, title) + html = self.fetch_url(url) if not html: return diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 42969e3ea..8dc3e24f0 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -41,6 +41,7 @@ google = lyrics.Google(MagicMock(), log) class LyricsPluginTest(unittest.TestCase): + def setUp(self): """Set up configuration.""" lyrics.LyricsPlugin() @@ -199,6 +200,7 @@ def url_to_filename(url): class MockFetchUrl(object): + def __init__(self, pathval='fetched_path'): self.pathval = pathval self.fetched = None @@ -224,6 +226,7 @@ LYRICS_TEXTS = confit.load_yaml(os.path.join(_common.RSRC, b'lyricstext.yaml')) class LyricsGoogleBaseTest(unittest.TestCase): + def setUp(self): """Set up configuration.""" try: @@ -244,7 +247,7 @@ class LyricsPluginSourcesTest(LyricsGoogleBaseTest): DEFAULT_SOURCES = [ dict(DEFAULT_SONG, backend=lyrics.LyricsWiki), # dict(artist=u'Santana', title=u'Black magic woman', - # backend=lyrics.MusiXmatch), + # backend=lyrics.MusiXmatch), # dict(DEFAULT_SONG, backend=lyrics.Genius), ] @@ -359,7 +362,7 @@ class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): soup = BeautifulSoup(html, "html.parser", parse_only=SoupStrainer('title')) self.assertEqual(google.is_page_candidate(url, soup.title.string, - s['title'], s['artist']), True, url) + s['title'], s['artist']), True, url) def test_is_page_candidate_fuzzy_match(self): """Test matching html page title with song infos -- when song infos are @@ -371,11 +374,11 @@ class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): # very small diffs (typo) are ok eg 'beats' vs 'beets' with same artist self.assertEqual(google.is_page_candidate(url, url_title, s['title'], - s['artist']), True, url) + s['artist']), True, url) # reject different title url_title = u'example.com | seets bong lyrics by John doe' self.assertEqual(google.is_page_candidate(url, url_title, s['title'], - s['artist']), False, url) + s['artist']), False, url) def test_is_page_candidate_special_chars(self): """Ensure that `is_page_candidate` doesn't crash when the artist From 07af27e44b9c923e23673f46aa3de683b34d15ae Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Tue, 2 May 2017 23:40:25 +0200 Subject: [PATCH 29/52] Lyrics are last paragraph with class 'mxm-lyrics__content' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove ‘data-reactid’ from marker. --- beetsplug/lyrics.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index f75b157c7..39e1502ed 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -263,9 +263,8 @@ class MusiXmatch(SymbolsReplaced): html = self.fetch_url(url) if not html: return - lyrics = extract_text_between(html, - '

', - '

') + html_part = html.split('

Date: Tue, 2 May 2017 23:48:20 +0200 Subject: [PATCH 30/52] Restore beets module import --- beetsplug/lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 39e1502ed..cdaf102e3 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -52,7 +52,7 @@ except ImportError: from beets import plugins from beets import ui - +import beets DIV_RE = re.compile(r'<(/?)div>?', re.I) COMMENT_RE = re.compile(r'', re.S) From b3fbdbae5a76980f2d022539af88f44b41fc9597 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Wed, 3 May 2017 00:02:09 +0200 Subject: [PATCH 31/52] Fix flake8 --- test/test_lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 8dc3e24f0..3b260c482 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -362,7 +362,7 @@ class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): soup = BeautifulSoup(html, "html.parser", parse_only=SoupStrainer('title')) self.assertEqual(google.is_page_candidate(url, soup.title.string, - s['title'], s['artist']), True, url) + s['title'], s['artist']), True, url) def test_is_page_candidate_fuzzy_match(self): """Test matching html page title with song infos -- when song infos are From f53ab801b83d2d24444e3ae7de1bb11dfbe55dbe Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Wed, 3 May 2017 00:11:26 +0200 Subject: [PATCH 32/52] Add indent --- test/test_lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 3b260c482..e2e5958b1 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -362,7 +362,7 @@ class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): soup = BeautifulSoup(html, "html.parser", parse_only=SoupStrainer('title')) self.assertEqual(google.is_page_candidate(url, soup.title.string, - s['title'], s['artist']), True, url) + s['title'], s['artist']), True, url) def test_is_page_candidate_fuzzy_match(self): """Test matching html page title with song infos -- when song infos are From 49e548bdbc9501a4f635a9f5dbc822adfc91f6a7 Mon Sep 17 00:00:00 2001 From: Wolf Date: Tue, 2 May 2017 22:36:26 -0400 Subject: [PATCH 33/52] Remove mention of python-itunes The plugin is inactive and has been broken for months: https://github.com/beetbox/beets/issues/2371 Fixes https://github.com/beetbox/beets/issues/1610 --- docs/plugins/fetchart.rst | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index ee0014072..82085ca61 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -49,7 +49,7 @@ file. The available options are: (``enforce_ratio: 0.5%``). Default: ``no``. - **sources**: List of sources to search for images. An asterisk `*` expands to all available sources. - Default: ``filesystem coverart itunes amazon albumart``, i.e., everything but + Default: ``filesystem coverart amazon albumart``, i.e., everything but ``wikipedia``, ``google`` and ``fanarttv``. Enable those sources for more matches at the cost of some speed. They are searched in the given order, thus in the default config, no remote (Web) art source are queried if @@ -82,13 +82,13 @@ or `Pillow`_. .. _ImageMagick: http://www.imagemagick.org/ Here's an example that makes plugin select only images that contain *front* or -*back* keywords in their filenames and prioritizes the iTunes source over +*back* keywords in their filenames and prioritizes the Amazon source over others:: fetchart: cautious: true cover_names: front back - sources: itunes * + sources: amazon * Manually Fetching Album Art @@ -128,7 +128,7 @@ Album Art Sources ----------------- By default, this plugin searches for art in the local filesystem as well as on -the Cover Art Archive, the iTunes Store, Amazon, and AlbumArt.org, in that +the Cover Art Archive, Amazon, and AlbumArt.org, in that order. You can reorder the sources or remove some to speed up the process using the ``sources`` configuration option. @@ -146,18 +146,9 @@ described above. For "as-is" imports (and non-autotagged imports using the iTunes Store '''''''''''' -To use the iTunes Store as an art source, install the `python-itunes`_ -library. You can do this using `pip`_, like so:: +There is currently `no plugin`_ to use the iTunes Store as an art source. - $ pip install https://github.com/ocelma/python-itunes/archive/master.zip - -(There's currently `a problem`_ that prevents a plain ``pip install -python-itunes`` from working.) -Once the library is installed, the plugin will use it to search automatically. - -.. _a problem: https://github.com/ocelma/python-itunes/issues/9 -.. _python-itunes: https://github.com/ocelma/python-itunes -.. _pip: http://www.pip-installer.org/ +.. _no plugin: https://github.com/beetbox/beets/issues/2371 Google custom search '''''''''''''''''''' From 471d46d67e770ab06ab4f573cc9c2745b3a63a76 Mon Sep 17 00:00:00 2001 From: Mark Stenglein Date: Tue, 2 May 2017 23:41:31 -0400 Subject: [PATCH 34/52] web: __init__: _rep: Filter all bytes for serializer This commit fixes issue #2532 by filtering out any byte values added by any other extensions and decoding them to strings for the JSON serializer. It does not remove any of the keys, instead opting to just convert them. Signed-off-by: Mark Stenglein --- beetsplug/web/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 3c18ebd5d..1bea628f8 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -42,6 +42,10 @@ def _rep(obj, expand=False): else: del out['path'] + # Filter all bytes attributes and convert them to strings + for key in filter(lambda key: isinstance(out[key], bytes), out): + out[key] = out[key].decode('utf-8') + # Get the size (in bytes) of the backing file. This is useful # for the Tomahawk resolver API. try: From 8b4b64d7343e7341f077d67afbc5ab660b7e0c58 Mon Sep 17 00:00:00 2001 From: Wolf Date: Wed, 3 May 2017 00:48:52 -0400 Subject: [PATCH 35/52] Changelog for #2540 --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5c0d15e0f..fbcf33c13 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -100,6 +100,11 @@ Fixes: ignored. Thanks to :user:`discopatrick`. :bug:`2513` :bug:`2517` * When the SQLite database stops being accessible, we now print a friendly error message. Thanks to :user:`Mary011196`. :bug:`1676` :bug:`2508` +* :doc:`plugins/fetchart`'s iTunes Store artwork lookup no longer recommended + in documentation, as the unmaintained `python-itunes`_ is broken. Want + to adopt it? :bug:`2371` :bug:`1610` + +.. _python-itunes: https://github.com/ocelma/python-itunes 1.4.3 (January 9, 2017) From 8f32bfed82ef7fcf8a3fd88ffe305230bc37d711 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Wed, 3 May 2017 07:42:50 +0200 Subject: [PATCH 36/52] Reactivate test of LyricsCom and MusiXmatch sources --- test/test_lyrics.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/test_lyrics.py b/test/test_lyrics.py index e2e5958b1..a96551e75 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -246,9 +246,10 @@ class LyricsPluginSourcesTest(LyricsGoogleBaseTest): DEFAULT_SOURCES = [ dict(DEFAULT_SONG, backend=lyrics.LyricsWiki), - # dict(artist=u'Santana', title=u'Black magic woman', - # backend=lyrics.MusiXmatch), - # dict(DEFAULT_SONG, backend=lyrics.Genius), + dict(DEFAULT_SONG, backend=lyrics.LyricsCom), + dict(artist=u'Santana', title=u'Black magic woman', + backend=lyrics.MusiXmatch), + dict(DEFAULT_SONG, backend=lyrics.Genius), ] GOOGLE_SOURCES = [ @@ -361,8 +362,9 @@ class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): html = raw_backend.fetch_url(url) soup = BeautifulSoup(html, "html.parser", parse_only=SoupStrainer('title')) - self.assertEqual(google.is_page_candidate(url, soup.title.string, - s['title'], s['artist']), True, url) + self.assertEqual( + google.is_page_candidate(url, soup.title.string, + s['title'], s['artist']), True, url) def test_is_page_candidate_fuzzy_match(self): """Test matching html page title with song infos -- when song infos are From e7ea7ab5f22342136b2a8f038bbbac19a0c66555 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Wed, 3 May 2017 20:04:02 +0200 Subject: [PATCH 37/52] Update fetchart.rst --- docs/plugins/fetchart.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 82085ca61..97e88c03c 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -143,13 +143,6 @@ When you choose to apply changes during an import, beets will search for art as described above. For "as-is" imports (and non-autotagged imports using the ``-A`` flag), beets only looks for art on the local filesystem. -iTunes Store -'''''''''''' - -There is currently `no plugin`_ to use the iTunes Store as an art source. - -.. _no plugin: https://github.com/beetbox/beets/issues/2371 - Google custom search '''''''''''''''''''' From 9394e0ac63786b7f0296eb39d0885960e677ff23 Mon Sep 17 00:00:00 2001 From: Mark Stenglein Date: Wed, 3 May 2017 16:34:30 -0400 Subject: [PATCH 38/52] web: __init__: _rep: change to base64 encoding As suggested, this commit adds base64 encoding for the bytes encoding. Signed-off-by: Mark Stenglein --- beetsplug/web/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 1bea628f8..2afef7786 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -25,6 +25,7 @@ from flask import g from werkzeug.routing import BaseConverter, PathConverter import os import json +import base64 # Utilities. @@ -44,7 +45,7 @@ def _rep(obj, expand=False): # Filter all bytes attributes and convert them to strings for key in filter(lambda key: isinstance(out[key], bytes), out): - out[key] = out[key].decode('utf-8') + out[key] = base64.b64encode(out[key]).decode('ascii') # Get the size (in bytes) of the backing file. This is useful # for the Tomahawk resolver API. From 22f07b91e93267520870f55114841c886c7d057d Mon Sep 17 00:00:00 2001 From: Mark Stenglein Date: Wed, 3 May 2017 16:36:40 -0400 Subject: [PATCH 39/52] web: __init__: _rep: Make the looping more pythonic As suggested, changes around the for loop to make it a bit more pythonic by using an if loop inside a normal for loop. Signed-off-by: Mark Stenglein --- beetsplug/web/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 2afef7786..c154b0cf4 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -44,7 +44,8 @@ def _rep(obj, expand=False): del out['path'] # Filter all bytes attributes and convert them to strings - for key in filter(lambda key: isinstance(out[key], bytes), out): + for key, value in out.items(): + if isinstance(out[key], bytes): out[key] = base64.b64encode(out[key]).decode('ascii') # Get the size (in bytes) of the backing file. This is useful From 409f0709706db987f647d271e49144bae6ec852e Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Wed, 3 May 2017 22:54:09 +0200 Subject: [PATCH 40/52] Remove lyrics.com source --- beetsplug/lyrics.py | 36 +----------------------------------- docs/plugins/lyrics.rst | 5 ++--- test/test_lyrics.py | 1 - 3 files changed, 3 insertions(+), 39 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index cdaf102e3..113bed104 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -381,39 +381,6 @@ class LyricsWiki(SymbolsReplaced): return lyrics -class LyricsCom(Backend): - """Fetch lyrics from Lyrics.com.""" - - URL_PATTERN = 'http://www.lyrics.com/%s-lyrics-%s.html' - NOT_FOUND = ( - 'Sorry, we do not have the lyric', - 'Submit Lyrics', - ) - - @classmethod - def _encode(cls, s): - s = re.sub(r'[^\w\s-]', '', s) - s = re.sub(r'\s+', '-', s) - return super(LyricsCom, cls)._encode(s).lower() - - def fetch(self, artist, title): - url = self.build_url(artist, title) - html = self.fetch_url(url) - if not html: - return - lyrics = extract_text_between(html, '

', '
') - if not lyrics: - return - for not_found_str in self.NOT_FOUND: - if not_found_str in lyrics: - return - - parts = lyrics.split('\n---\nLyrics powered by', 1) - if parts: - return parts[0] - - def remove_credits(text): """Remove first/last line of text if it contains the word 'lyrics' eg 'Lyrics by songsdatabase.com' @@ -605,11 +572,10 @@ class Google(Backend): class LyricsPlugin(plugins.BeetsPlugin): - SOURCES = ['google', 'lyricwiki', 'lyrics.com', 'musixmatch'] + SOURCES = ['google', 'lyricwiki', 'musixmatch'] SOURCE_BACKENDS = { 'google': Google, 'lyricwiki': LyricsWiki, - 'lyrics.com': LyricsCom, 'musixmatch': MusiXmatch, 'genius': Genius, } diff --git a/docs/plugins/lyrics.rst b/docs/plugins/lyrics.rst index 7263304f2..d7c268c7e 100644 --- a/docs/plugins/lyrics.rst +++ b/docs/plugins/lyrics.rst @@ -2,11 +2,10 @@ Lyrics Plugin ============= The ``lyrics`` plugin fetches and stores song lyrics from databases on the Web. -Namely, the current version of the plugin uses `Lyric Wiki`_, `Lyrics.com`_, +Namely, the current version of the plugin uses `Lyric Wiki`_, `Musixmatch`_, `Genius.com`_, and, optionally, the Google custom search API. .. _Lyric Wiki: http://lyrics.wikia.com/ -.. _Lyrics.com: http://www.lyrics.com/ .. _Musixmatch: https://www.musixmatch.com/ .. _Genius.com: http://genius.com/ @@ -60,7 +59,7 @@ configuration file. The available options are: sources known to be scrapeable. - **sources**: List of sources to search for lyrics. An asterisk ``*`` expands to all available sources. - Default: ``google lyricwiki lyrics.com musixmatch``, i.e., all the + Default: ``google lyricwiki musixmatch``, i.e., all the sources except for `genius`. The `google` source will be automatically deactivated if no ``google_API_key`` is setup. diff --git a/test/test_lyrics.py b/test/test_lyrics.py index a96551e75..e811da8d7 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -246,7 +246,6 @@ class LyricsPluginSourcesTest(LyricsGoogleBaseTest): DEFAULT_SOURCES = [ dict(DEFAULT_SONG, backend=lyrics.LyricsWiki), - dict(DEFAULT_SONG, backend=lyrics.LyricsCom), dict(artist=u'Santana', title=u'Black magic woman', backend=lyrics.MusiXmatch), dict(DEFAULT_SONG, backend=lyrics.Genius), From 6fa1651d99294378f0590deabf35e3ece524c197 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Wed, 3 May 2017 23:06:43 +0200 Subject: [PATCH 41/52] Update changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index fbcf33c13..1014bfd1b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -103,6 +103,7 @@ Fixes: * :doc:`plugins/fetchart`'s iTunes Store artwork lookup no longer recommended in documentation, as the unmaintained `python-itunes`_ is broken. Want to adopt it? :bug:`2371` :bug:`1610` +* :doc:`/plugins/lyrics`: drop Lyrics.com backend (don't work anymore) .. _python-itunes: https://github.com/ocelma/python-itunes From b57c49d7386e9e55ddf0495f034b93cb2da55588 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 4 May 2017 09:29:27 -0400 Subject: [PATCH 42/52] Add a period to a comment, simplify one expression w.r.t. #2542 --- beetsplug/web/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index c154b0cf4..290f25b48 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -43,10 +43,10 @@ def _rep(obj, expand=False): else: del out['path'] - # Filter all bytes attributes and convert them to strings + # Filter all bytes attributes and convert them to strings. for key, value in out.items(): if isinstance(out[key], bytes): - out[key] = base64.b64encode(out[key]).decode('ascii') + out[key] = base64.b64encode(value).decode('ascii') # Get the size (in bytes) of the backing file. This is useful # for the Tomahawk resolver API. From 376c31a2a41e0c71eefec14a8729c666d642420c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 4 May 2017 09:30:54 -0400 Subject: [PATCH 43/52] Changelog for #2542, which fixes #2532 --- docs/changelog.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index fbcf33c13..e5701cfaf 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -100,9 +100,11 @@ Fixes: ignored. Thanks to :user:`discopatrick`. :bug:`2513` :bug:`2517` * When the SQLite database stops being accessible, we now print a friendly error message. Thanks to :user:`Mary011196`. :bug:`1676` :bug:`2508` -* :doc:`plugins/fetchart`'s iTunes Store artwork lookup no longer recommended +* :doc:`/plugins/fetchart`'s iTunes Store artwork lookup no longer recommended in documentation, as the unmaintained `python-itunes`_ is broken. Want to adopt it? :bug:`2371` :bug:`1610` +* :doc:`/plugins/web`: Avoid a crash when sending binary data, such as + Chromaprint fingerprints, in music attributes. :bug:`2542` :bug:`2532` .. _python-itunes: https://github.com/ocelma/python-itunes From 6ab7ad22de68f5ce8f09fc671bcc117830698cc9 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 4 May 2017 09:35:19 -0400 Subject: [PATCH 44/52] Improve changelog for #2549, which fixes #2548 --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5c0915c3b..b3aba9511 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -106,6 +106,9 @@ Fixes: * :doc:`/plugins/web`: Avoid a crash when sending binary data, such as Chromaprint fingerprints, in music attributes. :bug:`2542` :bug:`2532` * :doc:`/plugins/lyrics`: drop Lyrics.com backend (don't work anymore) +* :doc:`/plugins/lyrics`: The Lyrics.com backend has been removed. (It stopped + working because of changes to the site's URL structure.) + :bug:`2548` :bug:`2549` .. _python-itunes: https://github.com/ocelma/python-itunes From fc560ac1cb7163cd2a5a9f64b7a00585caa8eb44 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 4 May 2017 09:41:20 -0400 Subject: [PATCH 45/52] Rearrange changelog for feature removal c.f. #2540 and #2549 --- docs/changelog.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b3aba9511..bf0d7d254 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -100,15 +100,17 @@ Fixes: ignored. Thanks to :user:`discopatrick`. :bug:`2513` :bug:`2517` * When the SQLite database stops being accessible, we now print a friendly error message. Thanks to :user:`Mary011196`. :bug:`1676` :bug:`2508` -* :doc:`/plugins/fetchart`'s iTunes Store artwork lookup no longer recommended - in documentation, as the unmaintained `python-itunes`_ is broken. Want - to adopt it? :bug:`2371` :bug:`1610` * :doc:`/plugins/web`: Avoid a crash when sending binary data, such as Chromaprint fingerprints, in music attributes. :bug:`2542` :bug:`2532` -* :doc:`/plugins/lyrics`: drop Lyrics.com backend (don't work anymore) + +Two plugins had backends removed due to bitrot: + * :doc:`/plugins/lyrics`: The Lyrics.com backend has been removed. (It stopped working because of changes to the site's URL structure.) :bug:`2548` :bug:`2549` +* :doc:`/plugins/fetchart`: The documentation no longer recommends iTunes + Store artwork lookup because the unmaintained `python-itunes`_ is broken. + Want to adopt it? :bug:`2371` :bug:`1610` .. _python-itunes: https://github.com/ocelma/python-itunes From 09ea5448f1aab3bc662127cfb6e5e38eac3c7798 Mon Sep 17 00:00:00 2001 From: "Shen-Ta Hsieh(BestSteve)" Date: Sat, 6 May 2017 02:52:55 +0800 Subject: [PATCH 46/52] Recompress png file (#2552) Signed-off-by: Shen-Ta Hsieh --- docs/plugins/beetsweb.png | Bin 36607 -> 33782 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/plugins/beetsweb.png b/docs/plugins/beetsweb.png index c335104eb86d66e48cda9a311ce8793eaeadf259..dcb5aae17a25a50eb44c9fcbdfd6360280c25a2d 100644 GIT binary patch literal 33782 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 Date: Fri, 12 May 2017 11:36:30 +0200 Subject: [PATCH 47/52] Corrected line length --- beets/autotag/mb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index c2172d0ac..9da709160 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -219,7 +219,8 @@ def track_info(recording, index=None, medium=None, medium_index=None, lyricist.append(artist_relation['artist']['name']) elif type == 'composer': composer.append(artist_relation['artist']['name']) - composer_sort.append(artist_relation['artist']['sort-name']) + composer_sort.append( + artist_relation['artist']['sort-name']) if lyricist: info.lyricist = u', '.join(lyricist) if composer: From 9ccaad27d3f59cda60fcffb5dc0223cd9e10ae95 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 12 May 2017 10:42:50 -0400 Subject: [PATCH 48/52] Undo some noisy whitespace changes --- beets/mediafile.py | 1 + test/test_mediafile.py | 46 +++++++++++++++++++++--------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 234e0daba..9242ab1f1 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1650,6 +1650,7 @@ class MediaFile(object): StorageStyle('ARRANGER'), ASFStorageStyle('beets/Arranger'), ) + grouping = MediaField( MP3StorageStyle('TIT1'), MP4StorageStyle('\xa9grp'), diff --git a/test/test_mediafile.py b/test/test_mediafile.py index f1066c56f..18dcc11a3 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -318,29 +318,29 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, """ full_initial_tags = { - 'title': u'full', - 'artist': u'the artist', - 'album': u'the album', - 'genre': u'the genre', - 'composer': u'the composer', - 'grouping': u'the grouping', - 'year': 2001, - 'month': None, - 'day': None, - 'date': datetime.date(2001, 1, 1), - 'track': 2, - 'tracktotal': 3, - 'disc': 4, - 'disctotal': 5, - 'lyrics': u'the lyrics', - 'comments': u'the comments', - 'bpm': 6, - 'comp': True, - 'mb_trackid': '8b882575-08a5-4452-a7a7-cbb8a1531f9e', - 'mb_albumid': '9e873859-8aa4-4790-b985-5a953e8ef628', - 'mb_artistid': '7cf0ea9d-86b9-4dad-ba9e-2355a64899ea', - 'art': None, - 'label': u'the label', + 'title': u'full', + 'artist': u'the artist', + 'album': u'the album', + 'genre': u'the genre', + 'composer': u'the composer', + 'grouping': u'the grouping', + 'year': 2001, + 'month': None, + 'day': None, + 'date': datetime.date(2001, 1, 1), + 'track': 2, + 'tracktotal': 3, + 'disc': 4, + 'disctotal': 5, + 'lyrics': u'the lyrics', + 'comments': u'the comments', + 'bpm': 6, + 'comp': True, + 'mb_trackid': '8b882575-08a5-4452-a7a7-cbb8a1531f9e', + 'mb_albumid': '9e873859-8aa4-4790-b985-5a953e8ef628', + 'mb_artistid': '7cf0ea9d-86b9-4dad-ba9e-2355a64899ea', + 'art': None, + 'label': u'the label', } tag_fields = [ From 5eca5181065d462e1a8adb1bb1cc7099e65f0e65 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 12 May 2017 12:41:51 -0400 Subject: [PATCH 49/52] Changelog for #2529, which fixes #2519 --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index bf0d7d254..7edd51cc2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -57,6 +57,10 @@ New features: Thanks to :user:`jansol`. :bug:`2488` :bug:`2524` +* A new field, ``composer_sort``, is now supported and fetched from + MusicBrainz. + Thanks to :user:`dosoe`. + :bug:`2519` :bug:`2529` Fixes: From 9840964f51fed3b3d0dcc1db9c76f90be4c05e73 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 16 May 2017 14:00:10 -0400 Subject: [PATCH 50/52] Fix #2562: avoid crash with newlines in templates Turns out! The $ character in Python regexes also matches before the last newline at the end of a string, not just at the end of a string. The \Z entity does what we really want: the *real* end of the string. --- beets/util/functemplate.py | 9 ++++++--- docs/changelog.rst | 1 + test/test_template.py | 5 +++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/beets/util/functemplate.py b/beets/util/functemplate.py index 58b0416a1..0e13db4a0 100644 --- a/beets/util/functemplate.py +++ b/beets/util/functemplate.py @@ -325,7 +325,7 @@ class Parser(object): # Common parsing resources. special_chars = (SYMBOL_DELIM, FUNC_DELIM, GROUP_OPEN, GROUP_CLOSE, ESCAPE_CHAR) - special_char_re = re.compile(r'[%s]|$' % + special_char_re = re.compile(r'[%s]|\Z' % u''.join(re.escape(c) for c in special_chars)) escapable_chars = (SYMBOL_DELIM, FUNC_DELIM, GROUP_CLOSE, ARG_SEP) terminator_chars = (GROUP_CLOSE,) @@ -343,8 +343,11 @@ class Parser(object): if self.in_argument: extra_special_chars = (ARG_SEP,) special_char_re = re.compile( - r'[%s]|$' % u''.join(re.escape(c) for c in - self.special_chars + extra_special_chars)) + r'[%s]|\Z' % u''.join( + re.escape(c) for c in + self.special_chars + extra_special_chars + ) + ) text_parts = [] diff --git a/docs/changelog.rst b/docs/changelog.rst index 7edd51cc2..9caca1280 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -106,6 +106,7 @@ Fixes: error message. Thanks to :user:`Mary011196`. :bug:`1676` :bug:`2508` * :doc:`/plugins/web`: Avoid a crash when sending binary data, such as Chromaprint fingerprints, in music attributes. :bug:`2542` :bug:`2532` +* Fix a hang when parsing templates that end in newlines. :bug:`2562` Two plugins had backends removed due to bitrot: diff --git a/test/test_template.py b/test/test_template.py index 1cbe9be0c..288bc2314 100644 --- a/test/test_template.py +++ b/test/test_template.py @@ -227,6 +227,11 @@ class ParseTest(unittest.TestCase): self.assertEqual(parts[2], u',') self._assert_symbol(parts[3], u"bar") + def test_newline_at_end(self): + parts = list(_normparse(u'foo\n')) + self.assertEqual(len(parts), 1) + self.assertEqual(parts[0], u'foo\n') + class EvalTest(unittest.TestCase): def _eval(self, template): From 060041a69e4646b87dec85e0125f6d8a41648152 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 17 May 2017 10:19:18 -0400 Subject: [PATCH 51/52] Load YAML as binary data This lets the YAML library itself deal with the encoding (mostly), which should address #2456 and #2565, which have to do with `open` giving us a system-specific encoding by default on Python 3 on Windows when the files should have been written using UTF-8 per the YAML standard. --- beets/util/confit.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/beets/util/confit.py b/beets/util/confit.py index 373e05ffc..73ae97abc 100644 --- a/beets/util/confit.py +++ b/beets/util/confit.py @@ -668,7 +668,7 @@ def load_yaml(filename): parsed, a ConfigReadError is raised. """ try: - with open(filename, 'r') as f: + with open(filename, 'rb') as f: return yaml.load(f, Loader=Loader) except (IOError, yaml.error.YAMLError) as exc: raise ConfigReadError(filename, exc) @@ -908,9 +908,10 @@ class Configuration(RootView): default_source = source break if default_source and default_source.filename: - with open(default_source.filename, 'r') as fp: + with open(default_source.filename, 'rb') as fp: default_data = fp.read() - yaml_out = restore_yaml_comments(yaml_out, default_data) + yaml_out = restore_yaml_comments(yaml_out, + default_data.decode('utf8')) return yaml_out From 35dd6fdf217aca5f2685df9a379ff79f238d396e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 17 May 2017 10:22:44 -0400 Subject: [PATCH 52/52] Changelog for #2566 --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9caca1280..caea9c6a4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -107,6 +107,8 @@ Fixes: * :doc:`/plugins/web`: Avoid a crash when sending binary data, such as Chromaprint fingerprints, in music attributes. :bug:`2542` :bug:`2532` * Fix a hang when parsing templates that end in newlines. :bug:`2562` +* Fix a crash when reading non-ASCII characters in configuration files on + Windows under Python 3. :bug:`2456` :bug:`2565` :bug:`2566` Two plugins had backends removed due to bitrot: