From 43fcb3d908cdc73314030f48527b142fb97d2041 Mon Sep 17 00:00:00 2001 From: tigranl Date: Mon, 5 Dec 2016 19:09:44 +0300 Subject: [PATCH 001/193] Check python version and enable https where it's possible --- beets/autotag/mb.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 78d382d87..8ceec9d87 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -28,9 +28,14 @@ import beets from beets import util from beets import config import six +import sys VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' -BASE_URL = 'http://musicbrainz.org/' + +if sys.version_info > (2, 7, 9): + BASE_URL = 'https://musicbrainz.org/' +else: + BASE_URL = 'http://musicbrainz.org/' musicbrainzngs.set_useragent('beets', beets.__version__, 'http://beets.io/') From 6ba5099034358675d4a3cd71db5b6c38a722b38e Mon Sep 17 00:00:00 2001 From: tigranl Date: Tue, 6 Dec 2016 16:17:25 +0300 Subject: [PATCH 002/193] Python version check for lyrics.py --- beetsplug/lyrics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 645d52559..7cd65b045 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -648,7 +648,8 @@ class LyricsPlugin(plugins.BeetsPlugin): params = { 'client_id': 'beets', 'client_secret': self.config['bing_client_secret'], - 'scope': 'http://api.microsofttranslator.com', + 'scope': 'https://api.microsofttranslator.com' if sys.version_info >= (2, 7, 9) else + 'http://api.microsofttranslator.com', 'grant_type': 'client_credentials', } From f60c911ffc0b5dca7247c7ed2d136decbbbf940c Mon Sep 17 00:00:00 2001 From: tigranl Date: Tue, 6 Dec 2016 17:10:35 +0300 Subject: [PATCH 003/193] Add SNI_SUPPORTED variable for https check --- beets/ui/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index ae30a9c60..5e852262b 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -43,6 +43,8 @@ from beets.autotag import mb from beets.dbcore import query as db_query import six +SNI_SUPPORTED = sys.version_info + # On Windows platforms, use colorama to support "ANSI" terminal colors. if sys.platform == 'win32': try: From 73a7a4ff675894a542a5eade7c27be30d0c58a6a Mon Sep 17 00:00:00 2001 From: tigranl Date: Tue, 6 Dec 2016 17:10:35 +0300 Subject: [PATCH 004/193] Add SNI_SUPPORTED variable for https check --- beetsplug/fetchart.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 27ffa49cb..b03ed9788 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -292,8 +292,12 @@ class RemoteArtSource(ArtSource): class CoverArtArchive(RemoteArtSource): NAME = u"Cover Art Archive" - URL = 'http://coverartarchive.org/release/{mbid}/front' - GROUP_URL = 'http://coverartarchive.org/release-group/{mbid}/front' + if ui.SNI_SUPPORTED >= (2, 7, 9): + URL = 'https://coverartarchive.org/release/{mbid}/front' + GROUP_URL = 'https://coverartarchive.org/release-group/{mbid}/front' + else: + URL = 'http://coverartarchive.org/release/{mbid}/front' + GROUP_URL = 'http://coverartarchive.org/release-group/{mbid}/front' def get(self, album, extra): """Return the Cover Art Archive and Cover Art Archive release group URLs @@ -310,7 +314,10 @@ class CoverArtArchive(RemoteArtSource): class Amazon(RemoteArtSource): NAME = u"Amazon" - URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' + if ui.SNI_SUPPORTED >= (2, 7, 9): + URL = 'https://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' + else: + URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' INDICES = (1, 2) def get(self, album, extra): @@ -324,7 +331,10 @@ class Amazon(RemoteArtSource): class AlbumArtOrg(RemoteArtSource): NAME = u"AlbumArt.org scraper" - URL = 'http://www.albumart.org/index_detail.php' + if ui.SNI_SUPPORTED >= (2, 7, 9): + URL = 'https://www.albumart.org/index_detail.php' + else: + URL = 'http://www.albumart.org/index_detail.php' PAT = r'href\s*=\s*"([^>"]*)"[^>]*title\s*=\s*"View larger image"' def get(self, album, extra): @@ -394,8 +404,10 @@ class GoogleImages(RemoteArtSource): class FanartTV(RemoteArtSource): """Art from fanart.tv requested using their API""" NAME = u"fanart.tv" - - API_URL = 'http://webservice.fanart.tv/v3/' + if ui.SNI_SUPPORTED >= (2, 7, 9): + API_URL = 'https://webservice.fanart.tv/v3/' + else: + API_URL = 'htts://webservice.fanart.tv/v3/' API_ALBUMS = API_URL + 'music/albums/' PROJECT_KEY = '61a7d0ab4e67162b7a0c7c35915cd48e' @@ -488,8 +500,12 @@ class ITunesStore(RemoteArtSource): class Wikipedia(RemoteArtSource): NAME = u"Wikipedia (queried through DBpedia)" - DBPEDIA_URL = 'http://dbpedia.org/sparql' - WIKIPEDIA_URL = 'http://en.wikipedia.org/w/api.php' + if ui.SNI_SUPPORTED >= (2, 7, 9): + DBPEDIA_URL = 'https://dbpedia.org/sparql' + WIKIPEDIA_URL = 'https://en.wikipedia.org/w/api.php' + else: + DBPEDIA_URL = 'http://dbpedia.org/sparql' + WIKIPEDIA_URL = 'http://en.wikipedia.org/w/api.php' SPARQL_QUERY = u'''PREFIX rdf: PREFIX dbpprop: PREFIX owl: From d065b33a816fa3aceef1aae214957a1c40d4b978 Mon Sep 17 00:00:00 2001 From: tigranl Date: Tue, 6 Dec 2016 17:10:35 +0300 Subject: [PATCH 005/193] Add SNI_SUPPORTED variable for https check --- beetsplug/lastimport.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 0ed9daf3c..1bf21c7d4 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -23,7 +23,10 @@ from beets import config from beets import plugins from beets.dbcore import types -API_URL = 'http://ws.audioscrobbler.com/2.0/' +if ui.SNI_SUPPORTED >= (2, 7, 9): + API_URL = 'https://ws.audioscrobbler.com/2.0/' +else: + API_URL = 'https://ws.audioscrobbler.com/2.0/' class LastImportPlugin(plugins.BeetsPlugin): From 5ae13764d88adcc9159c2ef8cdc40115a3ab23c4 Mon Sep 17 00:00:00 2001 From: tigranl Date: Tue, 6 Dec 2016 17:10:35 +0300 Subject: [PATCH 006/193] Add SNI_SUPPORTED variable for https check --- beets/util/artresizer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 4c2e92532..4cedf96d3 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -25,6 +25,7 @@ from tempfile import NamedTemporaryFile from six.moves.urllib.parse import urlencode from beets import logging from beets import util +from beets import ui import six # Resizing methods @@ -32,7 +33,10 @@ PIL = 1 IMAGEMAGICK = 2 WEBPROXY = 3 -PROXY_URL = 'http://images.weserv.nl/' +if ui.SNI_SUPPORTED >= (2, 7, 9): + PROXY_URL = 'https://images.weserv.nl/' +else: + PROXY_URL = 'http://images.weserv.nl/' log = logging.getLogger('beets') From 25ebf8948fcbf5d003a699be3aab8467fd095304 Mon Sep 17 00:00:00 2001 From: tigranl Date: Tue, 6 Dec 2016 17:10:35 +0300 Subject: [PATCH 007/193] Revert "Add SNI_SUPPORTED variable for https check" This reverts commit f60c911ffc0b5dca7247c7ed2d136decbbbf940c. --- beets/ui/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 5e852262b..ae30a9c60 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -43,8 +43,6 @@ from beets.autotag import mb from beets.dbcore import query as db_query import six -SNI_SUPPORTED = sys.version_info - # On Windows platforms, use colorama to support "ANSI" terminal colors. if sys.platform == 'win32': try: From 91819b2c80799aa9cb6f942676b4150f8356bc6f Mon Sep 17 00:00:00 2001 From: tigranl Date: Tue, 6 Dec 2016 19:31:42 +0300 Subject: [PATCH 008/193] Add SNI_SUPPORTED --- beets/util/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 5e1c30df1..dec97153e 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -33,6 +33,7 @@ import six MAX_FILENAME_LENGTH = 200 WINDOWS_MAGIC_PREFIX = u'\\\\?\\' +SNI_SUPPORTED = sys.version_info >= (2, 7, 9) class HumanReadableException(Exception): From efa90416a0437859c12a29709feaa1f01de3c030 Mon Sep 17 00:00:00 2001 From: tigranl Date: Tue, 6 Dec 2016 19:31:42 +0300 Subject: [PATCH 009/193] Add SNI_SUPPORTED Add SNI_SUPPORTED --- beetsplug/fetchart.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index b03ed9788..b852eba17 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -292,7 +292,7 @@ class RemoteArtSource(ArtSource): class CoverArtArchive(RemoteArtSource): NAME = u"Cover Art Archive" - if ui.SNI_SUPPORTED >= (2, 7, 9): + if uti.SNI_SUPPORTED: URL = 'https://coverartarchive.org/release/{mbid}/front' GROUP_URL = 'https://coverartarchive.org/release-group/{mbid}/front' else: @@ -314,7 +314,7 @@ class CoverArtArchive(RemoteArtSource): class Amazon(RemoteArtSource): NAME = u"Amazon" - if ui.SNI_SUPPORTED >= (2, 7, 9): + if util.SNI_SUPPORTED: URL = 'https://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' else: URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' @@ -331,7 +331,7 @@ class Amazon(RemoteArtSource): class AlbumArtOrg(RemoteArtSource): NAME = u"AlbumArt.org scraper" - if ui.SNI_SUPPORTED >= (2, 7, 9): + if util.SNI_SUPPORTED: URL = 'https://www.albumart.org/index_detail.php' else: URL = 'http://www.albumart.org/index_detail.php' @@ -404,7 +404,7 @@ class GoogleImages(RemoteArtSource): class FanartTV(RemoteArtSource): """Art from fanart.tv requested using their API""" NAME = u"fanart.tv" - if ui.SNI_SUPPORTED >= (2, 7, 9): + if util.SNI_SUPPORTED: API_URL = 'https://webservice.fanart.tv/v3/' else: API_URL = 'htts://webservice.fanart.tv/v3/' @@ -500,7 +500,7 @@ class ITunesStore(RemoteArtSource): class Wikipedia(RemoteArtSource): NAME = u"Wikipedia (queried through DBpedia)" - if ui.SNI_SUPPORTED >= (2, 7, 9): + if uti.SNI_SUPPORTED: DBPEDIA_URL = 'https://dbpedia.org/sparql' WIKIPEDIA_URL = 'https://en.wikipedia.org/w/api.php' else: From 420c451928840f9fe824351361e63935a40cfa00 Mon Sep 17 00:00:00 2001 From: tigranl Date: Tue, 6 Dec 2016 19:31:42 +0300 Subject: [PATCH 010/193] Add SNI_SUPPORTED Add SNI_SUPPORTED Add SNI_SUPPORTED --- beets/autotag/mb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 8ceec9d87..8d5641ca4 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -32,7 +32,7 @@ import sys VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' -if sys.version_info > (2, 7, 9): +if util.SNI_SUPPORTED: BASE_URL = 'https://musicbrainz.org/' else: BASE_URL = 'http://musicbrainz.org/' From 9bba178b5cf06df29afb73b71d5782c12ab8b0ed Mon Sep 17 00:00:00 2001 From: tigranl Date: Tue, 6 Dec 2016 19:31:42 +0300 Subject: [PATCH 011/193] Add tests for https --- test/test_art.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/test_art.py b/test/test_art.py index aba180780..393527eef 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -68,6 +68,7 @@ class FetchImageHelper(_common.TestCase): class FetchImageTest(FetchImageHelper, UseThePlugin): URL = 'http://example.com/test.jpg' + URL_HTTPS = 'https://example.com/test.jpg' def setUp(self): super(FetchImageTest, self).setUp() @@ -75,10 +76,13 @@ class FetchImageTest(FetchImageHelper, UseThePlugin): self.source = fetchart.RemoteArtSource(logger, self.plugin.config) self.extra = {'maxwidth': 0} self.candidate = fetchart.Candidate(logger, url=self.URL) + self.candidate_https = fetchart.Candidate(logger, url=self.URL_HTTPS) def test_invalid_type_returns_none(self): self.mock_response(self.URL, 'image/watercolour') + self.mock_response(self.URL_HTTPS, 'image/watercolour') self.source.fetch_image(self.candidate, self.extra) + self.source.fetch_image(self.candidate_https, self.extra) self.assertEqual(self.candidate.path, None) def test_jpeg_type_returns_path(self): @@ -88,13 +92,17 @@ class FetchImageTest(FetchImageHelper, UseThePlugin): def test_extension_set_by_content_type(self): self.mock_response(self.URL, 'image/png') + self.mock_response(self.URL_HTTPS, 'image/png') self.source.fetch_image(self.candidate, self.extra) + self.source.fetch_image(self.candidate_https, self.extra) self.assertEqual(os.path.splitext(self.candidate.path)[1], b'.png') self.assertExists(self.candidate.path) def test_does_not_rely_on_server_content_type(self): self.mock_response(self.URL, 'image/jpeg', 'image/png') + self.mock_response(self.URL_HTTPS, 'image/jpeg', 'imsge/png') self.source.fetch_image(self.candidate, self.extra) + self.source.fetch_image(self.candidate_https, self.extra) self.assertEqual(os.path.splitext(self.candidate.path)[1], b'.png') self.assertExists(self.candidate.path) @@ -157,6 +165,13 @@ class CombinedTest(FetchImageHelper, UseThePlugin): CAA_URL = 'http://coverartarchive.org/release/{0}/front' \ .format(MBID) + AMAZON_URL_HTTPS = 'http://images.amazon.com/images/P/{0}.01.LZZZZZZZ.jpg' \ + .format(ASIN) + AAO_URL_HTTPS = 'http://www.albumart.org/index_detail.php?asin={0}' \ + .format(ASIN) + CAA_URL_HTTPS = 'http://coverartarchive.org/release/{0}/front' \ + .format(MBID) + def setUp(self): super(CombinedTest, self).setUp() self.dpath = os.path.join(self.temp_dir, b'arttest') @@ -164,6 +179,7 @@ class CombinedTest(FetchImageHelper, UseThePlugin): def test_main_interface_returns_amazon_art(self): self.mock_response(self.AMAZON_URL) + self.mock_response(self.AMAZON_URL_HTTPS) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, None) self.assertIsNotNone(candidate) @@ -176,6 +192,7 @@ class CombinedTest(FetchImageHelper, UseThePlugin): def test_main_interface_gives_precedence_to_fs_art(self): _common.touch(os.path.join(self.dpath, b'art.jpg')) self.mock_response(self.AMAZON_URL) + self.mock_response(self.AMAZON_URL_HTTPS) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, [self.dpath]) self.assertIsNotNone(candidate) @@ -183,6 +200,7 @@ class CombinedTest(FetchImageHelper, UseThePlugin): def test_main_interface_falls_back_to_amazon(self): self.mock_response(self.AMAZON_URL) + self.mock_response(self.AMAZON_URL_HTTPS) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, [self.dpath]) self.assertIsNotNone(candidate) @@ -190,24 +208,30 @@ class CombinedTest(FetchImageHelper, UseThePlugin): def test_main_interface_tries_amazon_before_aao(self): self.mock_response(self.AMAZON_URL) + self.mock_response(self.AMAZON_URL_HTTPS) album = _common.Bag(asin=self.ASIN) self.plugin.art_for_album(album, [self.dpath]) self.assertEqual(len(responses.calls), 1) self.assertEqual(responses.calls[0].request.url, self.AMAZON_URL) + self.assertEqual(responses.calls[0].request.url, self.AMAZON_URL_HTTPS) def test_main_interface_falls_back_to_aao(self): self.mock_response(self.AMAZON_URL, content_type='text/html') + self.mock_response(self.AMAZON_URL_HTTPS, content_type='text/html') album = _common.Bag(asin=self.ASIN) self.plugin.art_for_album(album, [self.dpath]) self.assertEqual(responses.calls[-1].request.url, self.AAO_URL) + self.assertEqual(responses.calls[-1].request.url, self.AAO_URL_HTTPS) def test_main_interface_uses_caa_when_mbid_available(self): self.mock_response(self.CAA_URL) + self.mock_response(self.CAA_URL_HTTPS) album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN) candidate = self.plugin.art_for_album(album, None) self.assertIsNotNone(candidate) self.assertEqual(len(responses.calls), 1) self.assertEqual(responses.calls[0].request.url, self.CAA_URL) + self.assertEqual(responses.calls[0].request.url, self.CAA_URL_HTTPS) def test_local_only_does_not_access_network(self): album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN) From 21208b8c399287185a98aab9a85543dd6408e082 Mon Sep 17 00:00:00 2001 From: tigranl Date: Thu, 8 Dec 2016 19:09:15 +0300 Subject: [PATCH 012/193] Add SNI_SUPPORTED --- beets/util/artresizer.py | 3 +-- beetsplug/fetchart.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 4cedf96d3..e84b775dc 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -25,7 +25,6 @@ from tempfile import NamedTemporaryFile from six.moves.urllib.parse import urlencode from beets import logging from beets import util -from beets import ui import six # Resizing methods @@ -33,7 +32,7 @@ PIL = 1 IMAGEMAGICK = 2 WEBPROXY = 3 -if ui.SNI_SUPPORTED >= (2, 7, 9): +if util.SNI_SUPPORTED: PROXY_URL = 'https://images.weserv.nl/' else: PROXY_URL = 'http://images.weserv.nl/' diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index b852eba17..8af29ae75 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -292,7 +292,7 @@ class RemoteArtSource(ArtSource): class CoverArtArchive(RemoteArtSource): NAME = u"Cover Art Archive" - if uti.SNI_SUPPORTED: + if util.SNI_SUPPORTED: URL = 'https://coverartarchive.org/release/{mbid}/front' GROUP_URL = 'https://coverartarchive.org/release-group/{mbid}/front' else: @@ -500,7 +500,7 @@ class ITunesStore(RemoteArtSource): class Wikipedia(RemoteArtSource): NAME = u"Wikipedia (queried through DBpedia)" - if uti.SNI_SUPPORTED: + if util.SNI_SUPPORTED: DBPEDIA_URL = 'https://dbpedia.org/sparql' WIKIPEDIA_URL = 'https://en.wikipedia.org/w/api.php' else: From b65a7da8e2e6497ab1ef168e2073545875ec39d7 Mon Sep 17 00:00:00 2001 From: tigranl Date: Thu, 8 Dec 2016 19:20:18 +0300 Subject: [PATCH 013/193] Add SNI_SUPPORTED --- beetsplug/lastimport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 1bf21c7d4..c9cadc6a0 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -17,13 +17,13 @@ from __future__ import division, absolute_import, print_function import pylast from pylast import TopItem, _extract, _number -from beets import ui +from beets import util from beets import dbcore from beets import config from beets import plugins from beets.dbcore import types -if ui.SNI_SUPPORTED >= (2, 7, 9): +if util.SNI_SUPPORTED: API_URL = 'https://ws.audioscrobbler.com/2.0/' else: API_URL = 'https://ws.audioscrobbler.com/2.0/' From 68b4a03ecd96fa42b7afcedeb00e8859b43569fa Mon Sep 17 00:00:00 2001 From: tigranl Date: Sat, 10 Dec 2016 19:54:44 +0300 Subject: [PATCH 014/193] Add tests for https --- beetsplug/fetchart.py | 10 ++----- test/test_art.py | 69 +++++++++++++++++-------------------------- 2 files changed, 29 insertions(+), 50 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 8af29ae75..2ba5bcb7e 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -314,10 +314,7 @@ class CoverArtArchive(RemoteArtSource): class Amazon(RemoteArtSource): NAME = u"Amazon" - if util.SNI_SUPPORTED: - URL = 'https://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' - else: - URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' + URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' INDICES = (1, 2) def get(self, album, extra): @@ -331,10 +328,7 @@ class Amazon(RemoteArtSource): class AlbumArtOrg(RemoteArtSource): NAME = u"AlbumArt.org scraper" - if util.SNI_SUPPORTED: - URL = 'https://www.albumart.org/index_detail.php' - else: - URL = 'http://www.albumart.org/index_detail.php' + URL = 'http://www.albumart.org/index_detail.php' PAT = r'href\s*=\s*"([^>"]*)"[^>]*title\s*=\s*"View larger image"' def get(self, album, extra): diff --git a/test/test_art.py b/test/test_art.py index 393527eef..694d9a05c 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -67,42 +67,38 @@ class FetchImageHelper(_common.TestCase): class FetchImageTest(FetchImageHelper, UseThePlugin): - URL = 'http://example.com/test.jpg' - URL_HTTPS = 'https://example.com/test.jpg' + URL = '{0}example.com/test.jpg' def setUp(self): super(FetchImageTest, self).setUp() self.dpath = os.path.join(self.temp_dir, b'arttest') self.source = fetchart.RemoteArtSource(logger, self.plugin.config) self.extra = {'maxwidth': 0} - self.candidate = fetchart.Candidate(logger, url=self.URL) - self.candidate_https = fetchart.Candidate(logger, url=self.URL_HTTPS) + self.candidate = fetchart.Candidate(logger, url=self.URL.format('http://')) def test_invalid_type_returns_none(self): - self.mock_response(self.URL, 'image/watercolour') - self.mock_response(self.URL_HTTPS, 'image/watercolour') + self.mock_response(self.URL.format('http://'), 'image/watercolour') + self.mock_response(self.URL.format('https://'), 'image/watercolour') self.source.fetch_image(self.candidate, self.extra) - self.source.fetch_image(self.candidate_https, self.extra) self.assertEqual(self.candidate.path, None) def test_jpeg_type_returns_path(self): - self.mock_response(self.URL, 'image/jpeg') + self.mock_response(self.URL.format('http://'), 'image/jpeg') + self.mock_response(self.URL.format('https://')) self.source.fetch_image(self.candidate, self.extra) self.assertNotEqual(self.candidate.path, None) def test_extension_set_by_content_type(self): - self.mock_response(self.URL, 'image/png') - self.mock_response(self.URL_HTTPS, 'image/png') + self.mock_response(self.URL.format('http://'), 'image/png') + self.mock_response(self.URL.format('https://'), 'image/png') self.source.fetch_image(self.candidate, self.extra) - self.source.fetch_image(self.candidate_https, self.extra) self.assertEqual(os.path.splitext(self.candidate.path)[1], b'.png') self.assertExists(self.candidate.path) def test_does_not_rely_on_server_content_type(self): - self.mock_response(self.URL, 'image/jpeg', 'image/png') - self.mock_response(self.URL_HTTPS, 'image/jpeg', 'imsge/png') + self.mock_response(self.URL.format('http://'), 'image/jpeg', 'image/png') + self.mock_response(self.URL.format('https://'), 'image/jpeg', 'imsge/png') self.source.fetch_image(self.candidate, self.extra) - self.source.fetch_image(self.candidate_https, self.extra) self.assertEqual(os.path.splitext(self.candidate.path)[1], b'.png') self.assertExists(self.candidate.path) @@ -158,28 +154,20 @@ class FSArtTest(UseThePlugin): class CombinedTest(FetchImageHelper, UseThePlugin): ASIN = 'xxxx' MBID = 'releaseid' - AMAZON_URL = 'http://images.amazon.com/images/P/{0}.01.LZZZZZZZ.jpg' \ + AMAZON_URL = 'images.amazon.com/images/P/{0}.01.LZZZZZZZ.jpg' \ .format(ASIN) - AAO_URL = 'http://www.albumart.org/index_detail.php?asin={0}' \ + AAO_URL = 'www.albumart.org/index_detail.php?asin={0}' \ .format(ASIN) - CAA_URL = 'http://coverartarchive.org/release/{0}/front' \ + CAA_URL = 'coverartarchive.org/release/{0}/front' \ .format(MBID) - AMAZON_URL_HTTPS = 'http://images.amazon.com/images/P/{0}.01.LZZZZZZZ.jpg' \ - .format(ASIN) - AAO_URL_HTTPS = 'http://www.albumart.org/index_detail.php?asin={0}' \ - .format(ASIN) - CAA_URL_HTTPS = 'http://coverartarchive.org/release/{0}/front' \ - .format(MBID) - def setUp(self): super(CombinedTest, self).setUp() self.dpath = os.path.join(self.temp_dir, b'arttest') os.mkdir(self.dpath) def test_main_interface_returns_amazon_art(self): - self.mock_response(self.AMAZON_URL) - self.mock_response(self.AMAZON_URL_HTTPS) + self.mock_response("http://"+self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, None) self.assertIsNotNone(candidate) @@ -191,47 +179,44 @@ class CombinedTest(FetchImageHelper, UseThePlugin): def test_main_interface_gives_precedence_to_fs_art(self): _common.touch(os.path.join(self.dpath, b'art.jpg')) - self.mock_response(self.AMAZON_URL) - self.mock_response(self.AMAZON_URL_HTTPS) + self.mock_response("http://"+self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, [self.dpath]) self.assertIsNotNone(candidate) self.assertEqual(candidate.path, os.path.join(self.dpath, b'art.jpg')) def test_main_interface_falls_back_to_amazon(self): - self.mock_response(self.AMAZON_URL) - self.mock_response(self.AMAZON_URL_HTTPS) + self.mock_response("http://"+self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, [self.dpath]) self.assertIsNotNone(candidate) self.assertFalse(candidate.path.startswith(self.dpath)) def test_main_interface_tries_amazon_before_aao(self): - self.mock_response(self.AMAZON_URL) - self.mock_response(self.AMAZON_URL_HTTPS) + self.mock_response("http://"+self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) self.plugin.art_for_album(album, [self.dpath]) self.assertEqual(len(responses.calls), 1) - self.assertEqual(responses.calls[0].request.url, self.AMAZON_URL) - self.assertEqual(responses.calls[0].request.url, self.AMAZON_URL_HTTPS) + self.assertEqual(responses.calls[0].request.url, "http://"+self.AMAZON_URL) def test_main_interface_falls_back_to_aao(self): - self.mock_response(self.AMAZON_URL, content_type='text/html') - self.mock_response(self.AMAZON_URL_HTTPS, content_type='text/html') + self.mock_response("http://"+self.AMAZON_URL, content_type='text/html') album = _common.Bag(asin=self.ASIN) self.plugin.art_for_album(album, [self.dpath]) - self.assertEqual(responses.calls[-1].request.url, self.AAO_URL) - self.assertEqual(responses.calls[-1].request.url, self.AAO_URL_HTTPS) + self.assertEqual(responses.calls[-1].request.url, "http://"+self.AAO_URL) def test_main_interface_uses_caa_when_mbid_available(self): - self.mock_response(self.CAA_URL) - self.mock_response(self.CAA_URL_HTTPS) + self.mock_response("http://"+self.CAA_URL) + self.mock_response("https://"+self.CAA_URL) album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN) candidate = self.plugin.art_for_album(album, None) self.assertIsNotNone(candidate) self.assertEqual(len(responses.calls), 1) - self.assertEqual(responses.calls[0].request.url, self.CAA_URL) - self.assertEqual(responses.calls[0].request.url, self.CAA_URL_HTTPS) + if util.SNI_SUPPORTED: + url = "https://"+self.CAA_URL + else: + url = "http://"+self.CAA_URL + self.assertEqual(responses.calls[0].request.url, url) def test_local_only_does_not_access_network(self): album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN) From 0868299e92fb966c7d9c3351a7697693a74248eb Mon Sep 17 00:00:00 2001 From: tigranl Date: Sat, 10 Dec 2016 20:08:27 +0300 Subject: [PATCH 015/193] PEP8 corrections --- test/test_art.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/test/test_art.py b/test/test_art.py index 694d9a05c..bc3319cc2 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -74,7 +74,8 @@ class FetchImageTest(FetchImageHelper, UseThePlugin): self.dpath = os.path.join(self.temp_dir, b'arttest') self.source = fetchart.RemoteArtSource(logger, self.plugin.config) self.extra = {'maxwidth': 0} - self.candidate = fetchart.Candidate(logger, url=self.URL.format('http://')) + self.candidate = fetchart.Candidate(logger, + url=self.URL.format('http://')) def test_invalid_type_returns_none(self): self.mock_response(self.URL.format('http://'), 'image/watercolour') @@ -96,8 +97,10 @@ class FetchImageTest(FetchImageHelper, UseThePlugin): self.assertExists(self.candidate.path) def test_does_not_rely_on_server_content_type(self): - self.mock_response(self.URL.format('http://'), 'image/jpeg', 'image/png') - self.mock_response(self.URL.format('https://'), 'image/jpeg', 'imsge/png') + self.mock_response(self.URL.format('http://'), + 'image/jpeg', 'image/png') + self.mock_response(self.URL.format('https://'), + 'image/jpeg', 'imsge/png') self.source.fetch_image(self.candidate, self.extra) self.assertEqual(os.path.splitext(self.candidate.path)[1], b'.png') self.assertExists(self.candidate.path) @@ -167,7 +170,7 @@ class CombinedTest(FetchImageHelper, UseThePlugin): os.mkdir(self.dpath) def test_main_interface_returns_amazon_art(self): - self.mock_response("http://"+self.AMAZON_URL) + self.mock_response("http://" + self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, None) self.assertIsNotNone(candidate) @@ -179,43 +182,46 @@ class CombinedTest(FetchImageHelper, UseThePlugin): def test_main_interface_gives_precedence_to_fs_art(self): _common.touch(os.path.join(self.dpath, b'art.jpg')) - self.mock_response("http://"+self.AMAZON_URL) + self.mock_response("http://" + self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, [self.dpath]) self.assertIsNotNone(candidate) self.assertEqual(candidate.path, os.path.join(self.dpath, b'art.jpg')) def test_main_interface_falls_back_to_amazon(self): - self.mock_response("http://"+self.AMAZON_URL) + self.mock_response("http://" + self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, [self.dpath]) self.assertIsNotNone(candidate) self.assertFalse(candidate.path.startswith(self.dpath)) def test_main_interface_tries_amazon_before_aao(self): - self.mock_response("http://"+self.AMAZON_URL) + self.mock_response("http://" + self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) self.plugin.art_for_album(album, [self.dpath]) self.assertEqual(len(responses.calls), 1) - self.assertEqual(responses.calls[0].request.url, "http://"+self.AMAZON_URL) + self.assertEqual(responses.calls[0].request.url, + "http://" + self.AMAZON_URL) def test_main_interface_falls_back_to_aao(self): - self.mock_response("http://"+self.AMAZON_URL, content_type='text/html') + self.mock_response("http://" + self.AMAZON_URL, + content_type='text/html') album = _common.Bag(asin=self.ASIN) self.plugin.art_for_album(album, [self.dpath]) - self.assertEqual(responses.calls[-1].request.url, "http://"+self.AAO_URL) + self.assertEqual(responses.calls[-1].request.url, + "http://" + self.AAO_URL) def test_main_interface_uses_caa_when_mbid_available(self): - self.mock_response("http://"+self.CAA_URL) - self.mock_response("https://"+self.CAA_URL) + self.mock_response("http://" + self.CAA_URL) + self.mock_response("https://" + self.CAA_URL) album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN) candidate = self.plugin.art_for_album(album, None) self.assertIsNotNone(candidate) self.assertEqual(len(responses.calls), 1) if util.SNI_SUPPORTED: - url = "https://"+self.CAA_URL + url = "https://" + self.CAA_URL else: - url = "http://"+self.CAA_URL + url = "http://" + self.CAA_URL self.assertEqual(responses.calls[0].request.url, url) def test_local_only_does_not_access_network(self): From 471f875dc17bbd9bad0388671d51a828f0039073 Mon Sep 17 00:00:00 2001 From: tigranl Date: Sat, 10 Dec 2016 21:26:51 +0300 Subject: [PATCH 016/193] Fix typo --- beetsplug/fetchart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 2ba5bcb7e..855a1df10 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -401,7 +401,7 @@ class FanartTV(RemoteArtSource): if util.SNI_SUPPORTED: API_URL = 'https://webservice.fanart.tv/v3/' else: - API_URL = 'htts://webservice.fanart.tv/v3/' + API_URL = 'https://webservice.fanart.tv/v3/' API_ALBUMS = API_URL + 'music/albums/' PROJECT_KEY = '61a7d0ab4e67162b7a0c7c35915cd48e' From 5ca664e4aaeb9c666f7d95af85f73c356309406a Mon Sep 17 00:00:00 2001 From: tigranl Date: Sun, 11 Dec 2016 00:25:37 +0300 Subject: [PATCH 017/193] Fix typos --- beets/autotag/mb.py | 1 - beetsplug/lastimport.py | 1 + beetsplug/lyrics.py | 7 ++++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 8d5641ca4..88fa16c3a 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -28,7 +28,6 @@ import beets from beets import util from beets import config import six -import sys VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index c9cadc6a0..25624731e 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -18,6 +18,7 @@ from __future__ import division, absolute_import, print_function import pylast from pylast import TopItem, _extract, _number from beets import util +from beets import ui from beets import dbcore from beets import config from beets import plugins diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 7cd65b045..b837aef15 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -50,7 +50,7 @@ except ImportError: pass from beets import plugins -from beets import ui +from beets import util DIV_RE = re.compile(r'<(/?)div>?', re.I) @@ -645,11 +645,12 @@ class LyricsPlugin(plugins.BeetsPlugin): for source in sources] def get_bing_access_token(self): + url = "{0}api.microsofttranslator.com" params = { 'client_id': 'beets', 'client_secret': self.config['bing_client_secret'], - 'scope': 'https://api.microsofttranslator.com' if sys.version_info >= (2, 7, 9) else - 'http://api.microsofttranslator.com', + 'scope': url.format('https://') if util.SNI_SUPPORTED + else url.format('http://'), 'grant_type': 'client_credentials', } From dd115b13106ee9755ac2796179c91bde0b4545db Mon Sep 17 00:00:00 2001 From: tigranl Date: Sun, 11 Dec 2016 00:35:51 +0300 Subject: [PATCH 018/193] Add ui import --- beetsplug/lyrics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index b837aef15..feab2180e 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -51,6 +51,7 @@ except ImportError: from beets import plugins from beets import util +from beets import ui DIV_RE = re.compile(r'<(/?)div>?', re.I) From ab4246c5db8fe015b7928c262dc59917a3af0559 Mon Sep 17 00:00:00 2001 From: diomekes Date: Fri, 30 Dec 2016 13:08:56 -0500 Subject: [PATCH 019/193] add prompt choice to play items before import fix line number add comments --- beetsplug/play.py | 143 +++++++++++++++++++++++++++++++--------------- 1 file changed, 98 insertions(+), 45 deletions(-) diff --git a/beetsplug/play.py b/beetsplug/play.py index 4a2174909..2f708e5cb 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -19,17 +19,35 @@ from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets.ui import Subcommand +from beets.ui.commands import PromptChoice from beets import config from beets import ui from beets import util from os.path import relpath from tempfile import NamedTemporaryFile +import subprocess # Indicate where arguments should be inserted into the command string. # If this is missing, they're placed at the end. ARGS_MARKER = '$args' +def play(command_str, paths, open_args, keep_open=False): + """Play items in paths with command_str and optional arguments. If + keep_open, return to beets, otherwise exit once command runs. + """ + try: + if keep_open: + command = command_str.split() + command = command + open_args + subprocess.call(command) + else: + util.interactive_open(open_args, command_str) + except OSError as exc: + raise ui.UserError( + "Could not play the query: {0}".format(exc)) + + class PlayPlugin(BeetsPlugin): def __init__(self): @@ -40,11 +58,14 @@ class PlayPlugin(BeetsPlugin): 'use_folders': False, 'relative_to': None, 'raw': False, - # Backwards compatibility. See #1803 and line 74 + # Backwards compatibility. See #1803 and line 155 'warning_threshold': -2, 'warning_treshold': 100, }) + self.register_listener('before_choose_candidate', + self.before_choose_candidate_listener) + def commands(self): play_command = Subcommand( 'play', @@ -56,44 +77,17 @@ class PlayPlugin(BeetsPlugin): action='store', help=u'add additional arguments to the command', ) - play_command.func = self.play_music + play_command.func = self._play_command return [play_command] - def play_music(self, lib, opts, args): - """Execute query, create temporary playlist and execute player - command passing that playlist, at request insert optional arguments. + def _play_command(self, lib, opts, args): + """The CLI command function for `beet play`. Creates a list of paths + from query, determines if tracks or albums are to be played. """ - command_str = config['play']['command'].get() - if not command_str: - command_str = util.open_anything() use_folders = config['play']['use_folders'].get(bool) relative_to = config['play']['relative_to'].get() - raw = config['play']['raw'].get(bool) - warning_threshold = config['play']['warning_threshold'].get(int) - # We use -2 as a default value for warning_threshold to detect if it is - # set or not. We can't use a falsey value because it would have an - # actual meaning in the configuration of this plugin, and we do not use - # -1 because some people might use it as a value to obtain no warning, - # which wouldn't be that bad of a practice. - if warning_threshold == -2: - # if warning_threshold has not been set by user, look for - # warning_treshold, to preserve backwards compatibility. See #1803. - # warning_treshold has the correct default value of 100. - warning_threshold = config['play']['warning_treshold'].get(int) - if relative_to: relative_to = util.normpath(relative_to) - - # Add optional arguments to the player command. - if opts.args: - if ARGS_MARKER in command_str: - command_str = command_str.replace(ARGS_MARKER, opts.args) - else: - command_str = u"{} {}".format(command_str, opts.args) - else: - # Don't include the marker in the command. - command_str = command_str.replace(" " + ARGS_MARKER, "") - # Perform search by album and add folders rather than tracks to # playlist. if opts.album: @@ -117,14 +111,64 @@ class PlayPlugin(BeetsPlugin): paths = [relpath(path, relative_to) for path in paths] item_type = 'track' - item_type += 's' if len(selection) > 1 else '' - if not selection: ui.print_(ui.colorize('text_warning', u'No {0} to play.'.format(item_type))) return + open_args = self._playlist_or_paths(paths) + command_str = self._create_command_str(opts.args) + + # If user aborts due to long playlist: + cancel = self._print_info(selection, command_str, open_args, item_type) + + # Otherwise proceed with play command. + if not cancel: + play(command_str, paths, open_args) + + def _create_command_str(self, args=None): + """Creates a command string from the config command and optional args. + """ + command_str = config['play']['command'].get() + if not command_str: + return util.open_anything() + # Add optional arguments to the player command. + if args: + if ARGS_MARKER in command_str: + return command_str.replace(ARGS_MARKER, args) + else: + return u"{} {}".format(command_str, args) + else: + # Don't include the marker in the command. + return command_str.replace(" " + ARGS_MARKER, "") + + def _playlist_or_paths(self, paths): + """Returns either the raw paths of items or a playlist of the items. + """ + raw = config['play']['raw'].get(bool) + if raw: + return paths + else: + return [self._create_tmp_playlist(paths)] + + def _print_info(self, selection, command_str, open_args, + item_type='track'): + """Prompts user whether to continue if playlist exceeds threshold. + """ + warning_threshold = config['play']['warning_threshold'].get(int) + # We use -2 as a default value for warning_threshold to detect if it is + # set or not. We can't use a falsey value because it would have an + # actual meaning in the configuration of this plugin, and we do not use + # -1 because some people might use it as a value to obtain no warning, + # which wouldn't be that bad of a practice. + if warning_threshold == -2: + # if warning_threshold has not been set by user, look for + # warning_treshold, to preserve backwards compatibility. See #1803. + # warning_treshold has the correct default value of 100. + warning_threshold = config['play']['warning_treshold'].get(int) + # Warn user before playing any huge playlists. + item_type += 's' if len(selection) > 1 else '' if warning_threshold and len(selection) > warning_threshold: ui.print_(ui.colorize( 'text_warning', @@ -132,20 +176,11 @@ class PlayPlugin(BeetsPlugin): len(selection), item_type))) if ui.input_options((u'Continue', u'Abort')) == 'a': - return + return True + # Print number of tracks or albums to be played, log command to be run. ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) - if raw: - open_args = paths - else: - open_args = [self._create_tmp_playlist(paths)] - self._log.debug(u'executing command: {} {!r}', command_str, open_args) - try: - util.interactive_open(open_args, command_str) - except OSError as exc: - raise ui.UserError( - "Could not play the query: {0}".format(exc)) def _create_tmp_playlist(self, paths_list): """Create a temporary .m3u file. Return the filename. @@ -155,3 +190,21 @@ class PlayPlugin(BeetsPlugin): m3u.write(item + b'\n') m3u.close() return m3u.name + + def before_choose_candidate_listener(self, session, task): + """Append a "Play" choice to the interactive importer prompt. + """ + return [PromptChoice('y', 'plaY', self.importer_play)] + + def importer_play(self, session, task): + """Get items from current import task and send to play function. + """ + selection = task.items + paths = [item.path for item in selection] + + open_args = self._playlist_or_paths(paths) + command_str = self._create_command_str() + cancel = self._print_info(selection, command_str, open_args) + + if not cancel: + play(command_str, paths, open_args, keep_open=True) From af99ee21aacd0908c3de30032957571a1f9b2f18 Mon Sep 17 00:00:00 2001 From: diomekes Date: Sat, 31 Dec 2016 00:15:36 -0500 Subject: [PATCH 020/193] add documentation for play importer prompt choice --- docs/plugins/play.rst | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/plugins/play.rst b/docs/plugins/play.rst index 1ac51a1f3..6a34cda57 100644 --- a/docs/plugins/play.rst +++ b/docs/plugins/play.rst @@ -4,8 +4,8 @@ Play Plugin The ``play`` plugin allows you to pass the results of a query to a music player in the form of an m3u playlist or paths on the command line. -Usage ------ +Command Line Usage +------------------ To use the ``play`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then use it by invoking the ``beet play`` command with @@ -29,6 +29,18 @@ would on the command-line):: While playing you'll be able to interact with the player if it is a command-line oriented, and you'll get its output in real time. +Interactive Usage +----------------- + +The `play` plugin can also be invoked during an import. If enabled, the plugin +adds a `plaY` option to the prompt, so pressing `y` will execute the configured +command and play the items currently being imported. + +Once you exit your configured player, you will be returned to the import +decision prompt. If your player is configured to run in the background (in a +client/server setup), the music will play until you choose to stop it, and the +import operation continues immediately. + Configuration ------------- From 1c5c74f1d73fdb943074c6e89d2affb44511aa71 Mon Sep 17 00:00:00 2001 From: Tigran Kostandyan Date: Sat, 31 Dec 2016 18:46:01 +0300 Subject: [PATCH 021/193] Fix a typo --- beetsplug/lastimport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 25624731e..238e447bc 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -27,7 +27,7 @@ from beets.dbcore import types if util.SNI_SUPPORTED: API_URL = 'https://ws.audioscrobbler.com/2.0/' else: - API_URL = 'https://ws.audioscrobbler.com/2.0/' + API_URL = 'http://ws.audioscrobbler.com/2.0/' class LastImportPlugin(plugins.BeetsPlugin): From 0d68a126095bbc6969516f63d99c8a9e44399e55 Mon Sep 17 00:00:00 2001 From: Mike Cameron Date: Sun, 1 Jan 2017 04:36:48 -0500 Subject: [PATCH 022/193] Added alternate track field when importing from MusicBrains. Useful when importing multi-sided medias such as vinyls and cassettes. --- beets/autotag/__init__.py | 4 +++- beets/autotag/hooks.py | 3 ++- beets/autotag/mb.py | 5 +++-- beets/library.py | 7 ++++--- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 3e79a4498..eb3dae519 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -34,7 +34,7 @@ log = logging.getLogger('beets') def apply_item_metadata(item, track_info): """Set an item's metadata from its matched TrackInfo object. - """ + """ item.artist = track_info.artist item.artist_sort = track_info.artist_sort item.artist_credit = track_info.artist_credit @@ -157,3 +157,5 @@ def apply_metadata(album_info, mapping): item.composer = track_info.composer if track_info.arranger is not None: item.arranger = track_info.arranger + + item.alt_track_no = track_info.alt_track_no \ No newline at end of file diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 40db6e8d3..e64b42513 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -154,7 +154,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, arranger=None): + media=None, lyricist=None, composer=None, arranger=None, alt_track_no=None): self.title = title self.track_id = track_id self.artist = artist @@ -173,6 +173,7 @@ class TrackInfo(object): self.lyricist = lyricist self.composer = composer self.arranger = arranger + self.alt_track_no = alt_track_no # As above, work around a bug in python-musicbrainz-ngs. def decode(self, codec='utf-8'): diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 55ecfc185..aad342e98 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -252,8 +252,8 @@ def album_info(release): all_tracks = medium['track-list'] if 'pregap' in medium: all_tracks.insert(0, medium['pregap']) - - for track in all_tracks: + + for track in all_tracks: # Basic information from the recording. index += 1 ti = track_info( @@ -265,6 +265,7 @@ def album_info(release): ) ti.disctitle = disctitle ti.media = format + ti.alt_track_no = track['number'] # Prefer track data, where present, over recording data. if track.get('title'): diff --git a/beets/library.py b/beets/library.py index 32176b68d..d9ffd38fd 100644 --- a/beets/library.py +++ b/beets/library.py @@ -322,7 +322,7 @@ class LibModel(dbcore.Model): funcs.update(plugins.template_funcs()) return funcs - def store(self, fields=None): + def store(self, fields=None): super(LibModel, self).store(fields) plugins.send('database_change', lib=self._db, model=self) @@ -423,9 +423,10 @@ class Item(LibModel): 'month': types.PaddedInt(2), 'day': types.PaddedInt(2), 'track': types.PaddedInt(2), + 'alt_track_no': types.STRING, 'tracktotal': types.PaddedInt(2), 'disc': types.PaddedInt(2), - 'disctotal': types.PaddedInt(2), + 'disctotal': types.PaddedInt(2), 'lyrics': types.STRING, 'comments': types.STRING, 'bpm': types.INTEGER, @@ -456,7 +457,7 @@ class Item(LibModel): 'original_year': types.PaddedInt(4), 'original_month': types.PaddedInt(2), 'original_day': types.PaddedInt(2), - 'initial_key': MusicalKey(), + 'initial_key': MusicalKey(), 'length': DurationType(), 'bitrate': types.ScaledInt(1000, u'kbps'), From 76b3212e781bcaba85713e5d5c24751adfebb0d2 Mon Sep 17 00:00:00 2001 From: Mike Cameron Date: Sun, 1 Jan 2017 04:51:34 -0500 Subject: [PATCH 023/193] Added alternate track numbers for Discogs. --- beets/autotag/hooks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index e64b42513..a9a35e635 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -154,7 +154,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, alt_track_no=None): + media=None, lyricist=None, composer=None, arranger=None, + alt_track_no=None): self.title = title self.track_id = track_id self.artist = artist From 8a00791ecca90999b570f0d3f385d99f288413dd Mon Sep 17 00:00:00 2001 From: Mike Cameron Date: Sun, 1 Jan 2017 04:52:32 -0500 Subject: [PATCH 024/193] Oops. Forgot to actually stage the correct file. --- beetsplug/discogs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index cf10a2a36..905358024 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -314,7 +314,9 @@ class DiscogsPlugin(BeetsPlugin): # Only real tracks have `position`. Otherwise, it's an index track. if track['position']: index += 1 - tracks.append(self.get_track_info(track, index)) + ti = self.get_track_info(track, index) + ti.alt_track_no = track['position'] + tracks.append(ti) else: index_tracks[index + 1] = track['title'] From 869d0781d55c0d9c631f12e1009126ff18f18382 Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Mon, 2 Jan 2017 14:34:00 -0500 Subject: [PATCH 025/193] Absubmit add section on skipped file by the plugin Add a section to the absubmit plugin documentation about files that are skipped by this plugin. --- docs/plugins/absubmit.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/plugins/absubmit.rst b/docs/plugins/absubmit.rst index a8cd1b380..6519e6159 100644 --- a/docs/plugins/absubmit.rst +++ b/docs/plugins/absubmit.rst @@ -25,6 +25,8 @@ Type:: to run the analysis program and upload its results. This will work on any music with a MusicBrainz track ID attached. +The plugin skips any file missing a MusicBrainz ID (MBID). This should rarely happen as Beets tags all files with their MBID on import. Files of in an unsupported encoding format are also skipped. `streaming_extractor_music`_ currently supports files with the following extensions: ``mp3``, ``ogg``, ``oga``, ``flac``, ``mp4``, ``m4a``, ``m4r``, ``m4b``, ``m4p``, ``aac``, ``wma``, ``asf``, ``mpc``, ``wv``, ``spx``, ``tta``, ``3g2``, ``aif``, ``aiff`` and ``ape``. + Configuration ------------- From 4711bf708bd184fd5369bca335941c1627141ab2 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 2 Jan 2017 15:59:14 -0500 Subject: [PATCH 026/193] Docs tweaks for #2365 --- docs/plugins/absubmit.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/plugins/absubmit.rst b/docs/plugins/absubmit.rst index 6519e6159..665133c64 100644 --- a/docs/plugins/absubmit.rst +++ b/docs/plugins/absubmit.rst @@ -22,10 +22,14 @@ Type:: beet absubmit [QUERY] -to run the analysis program and upload its results. This will work on any -music with a MusicBrainz track ID attached. +to run the analysis program and upload its results. -The plugin skips any file missing a MusicBrainz ID (MBID). This should rarely happen as Beets tags all files with their MBID on import. Files of in an unsupported encoding format are also skipped. `streaming_extractor_music`_ currently supports files with the following extensions: ``mp3``, ``ogg``, ``oga``, ``flac``, ``mp4``, ``m4a``, ``m4r``, ``m4b``, ``m4p``, ``aac``, ``wma``, ``asf``, ``mpc``, ``wv``, ``spx``, ``tta``, ``3g2``, ``aif``, ``aiff`` and ``ape``. +The plugin works on music with a MusicBrainz track ID attached. The plugin +will also skip music that the analysis tool doesn't support. +`streaming_extractor_music`_ currently supports files with the extensions +``mp3``, ``ogg``, ``oga``, ``flac``, ``mp4``, ``m4a``, ``m4r``, ``m4b``, +``m4p``, ``aac``, ``wma``, ``asf``, ``mpc``, ``wv``, ``spx``, ``tta``, +``3g2``, ``aif``, ``aiff`` and ``ape``. Configuration ------------- From 3acd4480ffe8cabb19dd676caca7f699ed43ed8a Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 2 Jan 2017 18:29:37 -0500 Subject: [PATCH 027/193] Use a stdlib method for _to_epoch_time on py3 This also works around a bug in Python 3.6.0 on Windows: see #2358. --- beets/dbcore/query.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index d27897e69..470ca2ac6 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -503,9 +503,13 @@ def _to_epoch_time(date): """Convert a `datetime` object to an integer number of seconds since the (local) Unix epoch. """ - epoch = datetime.fromtimestamp(0) - delta = date - epoch - return int(delta.total_seconds()) + if hasattr(date, 'timestamp'): + # The `timestamp` method exists on Python 3.3+. + return int(date.timestamp()) + else: + epoch = datetime.fromtimestamp(0) + delta = date - epoch + return int(delta.total_seconds()) def _parse_periods(pattern): From bd340b29107200cde50529f83ec1680deb6831b5 Mon Sep 17 00:00:00 2001 From: Johnny Robeson Date: Mon, 2 Jan 2017 19:06:49 -0500 Subject: [PATCH 028/193] Replace Python 3.4 with 3.6 on Appveyor (#2358) Python 3.4 is quite old and offers little value on Windows. Closes #2344 --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 4f350b938..938d3a5a4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,10 +12,10 @@ environment: matrix: - PYTHON: C:\Python27 TOX_ENV: py27-test - - PYTHON: C:\Python34 - TOX_ENV: py34-test - PYTHON: C:\Python35 TOX_ENV: py35-test + - PYTHON: C:\Python36 + TOX_ENV: py36-test # Install Tox for running tests. install: From f941fd42de2966686301ceb657bd10b89febea08 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 2 Jan 2017 20:39:10 -0500 Subject: [PATCH 029/193] Always use SSL on servers that don't require SNI I did a little audit using the `openssl` command-line tool to find the servers that don't require SNI. Here's what I found: icbrainz.org: SNI images.weserv.nl: inconclusive, but docs say yes SNI coverartarchive.org: SNI webservice.fanart.tv: *no* SNI dbpedia.org: *no* SNI en.wikipedia.org: *no* SNI ws.audioscrobbler.com: *no* SNI api.microsofttranslator.com: *no* SNI In summary, *only* MusicBrainz and CoverArtArchive were found to require SNI. So I'm using SSL unconditionally on all the other sites. --- beetsplug/fetchart.py | 13 +++---------- beetsplug/lastimport.py | 6 +----- beetsplug/lyrics.py | 5 +---- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 855a1df10..d87a5dc48 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -398,10 +398,7 @@ class GoogleImages(RemoteArtSource): class FanartTV(RemoteArtSource): """Art from fanart.tv requested using their API""" NAME = u"fanart.tv" - if util.SNI_SUPPORTED: - API_URL = 'https://webservice.fanart.tv/v3/' - else: - API_URL = 'https://webservice.fanart.tv/v3/' + API_URL = 'https://webservice.fanart.tv/v3/' API_ALBUMS = API_URL + 'music/albums/' PROJECT_KEY = '61a7d0ab4e67162b7a0c7c35915cd48e' @@ -494,12 +491,8 @@ class ITunesStore(RemoteArtSource): class Wikipedia(RemoteArtSource): NAME = u"Wikipedia (queried through DBpedia)" - if util.SNI_SUPPORTED: - DBPEDIA_URL = 'https://dbpedia.org/sparql' - WIKIPEDIA_URL = 'https://en.wikipedia.org/w/api.php' - else: - DBPEDIA_URL = 'http://dbpedia.org/sparql' - WIKIPEDIA_URL = 'http://en.wikipedia.org/w/api.php' + DBPEDIA_URL = 'https://dbpedia.org/sparql' + WIKIPEDIA_URL = 'https://en.wikipedia.org/w/api.php' SPARQL_QUERY = u'''PREFIX rdf: PREFIX dbpprop: PREFIX owl: diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 238e447bc..d7b84b0aa 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -17,17 +17,13 @@ from __future__ import division, absolute_import, print_function import pylast from pylast import TopItem, _extract, _number -from beets import util from beets import ui from beets import dbcore from beets import config from beets import plugins from beets.dbcore import types -if util.SNI_SUPPORTED: - API_URL = 'https://ws.audioscrobbler.com/2.0/' -else: - API_URL = 'http://ws.audioscrobbler.com/2.0/' +API_URL = 'https://ws.audioscrobbler.com/2.0/' class LastImportPlugin(plugins.BeetsPlugin): diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index feab2180e..bce95759e 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -50,7 +50,6 @@ except ImportError: pass from beets import plugins -from beets import util from beets import ui @@ -646,12 +645,10 @@ class LyricsPlugin(plugins.BeetsPlugin): for source in sources] def get_bing_access_token(self): - url = "{0}api.microsofttranslator.com" params = { 'client_id': 'beets', 'client_secret': self.config['bing_client_secret'], - 'scope': url.format('https://') if util.SNI_SUPPORTED - else url.format('http://'), + 'scope': "https://api.microsofttranslator.com", 'grant_type': 'client_credentials', } From 33a8e81f08641b4dee8a1767ab45646271222f58 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 2 Jan 2017 20:49:12 -0500 Subject: [PATCH 030/193] Simplify test changes We don't need quite so many checks now that SSL isn't conditional most of the time. --- test/test_art.py | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/test/test_art.py b/test/test_art.py index bc3319cc2..50ff26b00 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -67,40 +67,33 @@ class FetchImageHelper(_common.TestCase): class FetchImageTest(FetchImageHelper, UseThePlugin): - URL = '{0}example.com/test.jpg' + URL = 'http://example.com/test.jpg' def setUp(self): super(FetchImageTest, self).setUp() self.dpath = os.path.join(self.temp_dir, b'arttest') self.source = fetchart.RemoteArtSource(logger, self.plugin.config) self.extra = {'maxwidth': 0} - self.candidate = fetchart.Candidate(logger, - url=self.URL.format('http://')) + self.candidate = fetchart.Candidate(logger, url=self.URL) def test_invalid_type_returns_none(self): - self.mock_response(self.URL.format('http://'), 'image/watercolour') - self.mock_response(self.URL.format('https://'), 'image/watercolour') + self.mock_response(self.URL, 'image/watercolour') self.source.fetch_image(self.candidate, self.extra) self.assertEqual(self.candidate.path, None) def test_jpeg_type_returns_path(self): - self.mock_response(self.URL.format('http://'), 'image/jpeg') - self.mock_response(self.URL.format('https://')) + self.mock_response(self.URL, 'image/jpeg') self.source.fetch_image(self.candidate, self.extra) self.assertNotEqual(self.candidate.path, None) def test_extension_set_by_content_type(self): - self.mock_response(self.URL.format('http://'), 'image/png') - self.mock_response(self.URL.format('https://'), 'image/png') + self.mock_response(self.URL, 'image/png') self.source.fetch_image(self.candidate, self.extra) self.assertEqual(os.path.splitext(self.candidate.path)[1], b'.png') self.assertExists(self.candidate.path) def test_does_not_rely_on_server_content_type(self): - self.mock_response(self.URL.format('http://'), - 'image/jpeg', 'image/png') - self.mock_response(self.URL.format('https://'), - 'image/jpeg', 'imsge/png') + self.mock_response(self.URL, 'image/jpeg', 'image/png') self.source.fetch_image(self.candidate, self.extra) self.assertEqual(os.path.splitext(self.candidate.path)[1], b'.png') self.assertExists(self.candidate.path) @@ -157,9 +150,9 @@ class FSArtTest(UseThePlugin): class CombinedTest(FetchImageHelper, UseThePlugin): ASIN = 'xxxx' MBID = 'releaseid' - AMAZON_URL = 'images.amazon.com/images/P/{0}.01.LZZZZZZZ.jpg' \ + AMAZON_URL = 'http://images.amazon.com/images/P/{0}.01.LZZZZZZZ.jpg' \ .format(ASIN) - AAO_URL = 'www.albumart.org/index_detail.php?asin={0}' \ + AAO_URL = 'http://www.albumart.org/index_detail.php?asin={0}' \ .format(ASIN) CAA_URL = 'coverartarchive.org/release/{0}/front' \ .format(MBID) @@ -170,7 +163,7 @@ class CombinedTest(FetchImageHelper, UseThePlugin): os.mkdir(self.dpath) def test_main_interface_returns_amazon_art(self): - self.mock_response("http://" + self.AMAZON_URL) + self.mock_response(self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, None) self.assertIsNotNone(candidate) @@ -182,34 +175,31 @@ class CombinedTest(FetchImageHelper, UseThePlugin): def test_main_interface_gives_precedence_to_fs_art(self): _common.touch(os.path.join(self.dpath, b'art.jpg')) - self.mock_response("http://" + self.AMAZON_URL) + self.mock_response(self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, [self.dpath]) self.assertIsNotNone(candidate) self.assertEqual(candidate.path, os.path.join(self.dpath, b'art.jpg')) def test_main_interface_falls_back_to_amazon(self): - self.mock_response("http://" + self.AMAZON_URL) + self.mock_response(self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) candidate = self.plugin.art_for_album(album, [self.dpath]) self.assertIsNotNone(candidate) self.assertFalse(candidate.path.startswith(self.dpath)) def test_main_interface_tries_amazon_before_aao(self): - self.mock_response("http://" + self.AMAZON_URL) + self.mock_response(self.AMAZON_URL) album = _common.Bag(asin=self.ASIN) self.plugin.art_for_album(album, [self.dpath]) self.assertEqual(len(responses.calls), 1) - self.assertEqual(responses.calls[0].request.url, - "http://" + self.AMAZON_URL) + self.assertEqual(responses.calls[0].request.url, self.AMAZON_URL) def test_main_interface_falls_back_to_aao(self): - self.mock_response("http://" + self.AMAZON_URL, - content_type='text/html') + self.mock_response(self.AMAZON_URL, content_type='text/html') album = _common.Bag(asin=self.ASIN) self.plugin.art_for_album(album, [self.dpath]) - self.assertEqual(responses.calls[-1].request.url, - "http://" + self.AAO_URL) + self.assertEqual(responses.calls[-1].request.url, self.AAO_URL) def test_main_interface_uses_caa_when_mbid_available(self): self.mock_response("http://" + self.CAA_URL) From 9e76c839f6869dd154c99b3cbe15c9a93d0b5783 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 2 Jan 2017 20:57:50 -0500 Subject: [PATCH 031/193] Changelog for #2307 Close #2247, close #2243, close #2245, close #2244, close #2248. --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index c5113bdfa..102154bbc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,9 @@ Features: Fixes: +* We now use SSL to access Web services whenever possible. That includes + MusicBrainz itself, several album art sources, some lyrics sources, and + other servers. Thanks to :user:`tigranl`. :bug:`2307` * :doc:`/plugins/bpd`: Fix a crash on non-ASCII MPD commands. :bug:`2332` * :doc:`/plugins/scrub`: Avoid a crash when files cannot be read or written. :bug:`2351` From d389ac15e156de8abd67b7a680000bf8788f2ccd Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 2 Jan 2017 21:00:01 -0500 Subject: [PATCH 032/193] Use HTTPS for MS translator API (from #2247) --- beetsplug/lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index a93b0f7a3..92abe377e 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -762,7 +762,7 @@ class LyricsPlugin(plugins.BeetsPlugin): if self.bing_auth_token: # Extract unique lines to limit API request size per song text_lines = set(text.split('\n')) - url = ('http://api.microsofttranslator.com/v2/Http.svc/' + url = ('https://api.microsofttranslator.com/v2/Http.svc/' 'Translate?text=%s&to=%s' % ('|'.join(text_lines), to_lang)) r = requests.get(url, headers={"Authorization ": self.bing_auth_token}) From 8d97257647ab313805b5583522316c3ce4be109a Mon Sep 17 00:00:00 2001 From: diomekes Date: Tue, 3 Jan 2017 22:10:35 -0500 Subject: [PATCH 033/193] use shlex_split, clean up code and docs --- beetsplug/play.py | 58 +++++++++++++++++++++++-------------------- docs/plugins/play.rst | 2 +- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/beetsplug/play.py b/beetsplug/play.py index 2f708e5cb..032894777 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -32,13 +32,19 @@ import subprocess ARGS_MARKER = '$args' -def play(command_str, paths, open_args, keep_open=False): +def play(command_str, selection, paths, open_args, log, item_type='track', + keep_open=False): """Play items in paths with command_str and optional arguments. If keep_open, return to beets, otherwise exit once command runs. """ + # Print number of tracks or albums to be played, log command to be run. + item_type += 's' if len(selection) > 1 else '' + ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) + log.debug(u'executing command: {} {!r}', command_str, open_args) + try: if keep_open: - command = command_str.split() + command = util.shlex_split(command_str) command = command + open_args subprocess.call(command) else: @@ -81,8 +87,8 @@ class PlayPlugin(BeetsPlugin): return [play_command] def _play_command(self, lib, opts, args): - """The CLI command function for `beet play`. Creates a list of paths - from query, determines if tracks or albums are to be played. + """The CLI command function for `beet play`. Create a list of paths + from query, determine if tracks or albums are to be played. """ use_folders = config['play']['use_folders'].get(bool) relative_to = config['play']['relative_to'].get() @@ -117,17 +123,17 @@ class PlayPlugin(BeetsPlugin): return open_args = self._playlist_or_paths(paths) - command_str = self._create_command_str(opts.args) + command_str = self._command_str(opts.args) - # If user aborts due to long playlist: - cancel = self._print_info(selection, command_str, open_args, item_type) + # Check if the selection exceeds configured threshold. If True, + # cancel, otherwise proceed with play command. + if not self._exceeds_threshold(selection, command_str, open_args, + item_type): + play(command_str, selection, paths, open_args, self._log, + item_type) - # Otherwise proceed with play command. - if not cancel: - play(command_str, paths, open_args) - - def _create_command_str(self, args=None): - """Creates a command string from the config command and optional args. + def _command_str(self, args=None): + """Create a command string from the config command and optional args. """ command_str = config['play']['command'].get() if not command_str: @@ -143,17 +149,18 @@ class PlayPlugin(BeetsPlugin): return command_str.replace(" " + ARGS_MARKER, "") def _playlist_or_paths(self, paths): - """Returns either the raw paths of items or a playlist of the items. + """Return either the raw paths of items or a playlist of the items. """ - raw = config['play']['raw'].get(bool) - if raw: + if config['play']['raw']: return paths else: return [self._create_tmp_playlist(paths)] - def _print_info(self, selection, command_str, open_args, - item_type='track'): - """Prompts user whether to continue if playlist exceeds threshold. + def _exceeds_threshold(self, selection, command_str, open_args, + item_type='track'): + """Prompt user whether to continue if playlist exceeds threshold. If + this returns True, the tracks or albums are not played, if False, + the play command is run. """ warning_threshold = config['play']['warning_threshold'].get(int) # We use -2 as a default value for warning_threshold to detect if it is @@ -168,7 +175,6 @@ class PlayPlugin(BeetsPlugin): warning_threshold = config['play']['warning_treshold'].get(int) # Warn user before playing any huge playlists. - item_type += 's' if len(selection) > 1 else '' if warning_threshold and len(selection) > warning_threshold: ui.print_(ui.colorize( 'text_warning', @@ -178,9 +184,7 @@ class PlayPlugin(BeetsPlugin): if ui.input_options((u'Continue', u'Abort')) == 'a': return True - # Print number of tracks or albums to be played, log command to be run. - ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) - self._log.debug(u'executing command: {} {!r}', command_str, open_args) + return False def _create_tmp_playlist(self, paths_list): """Create a temporary .m3u file. Return the filename. @@ -203,8 +207,8 @@ class PlayPlugin(BeetsPlugin): paths = [item.path for item in selection] open_args = self._playlist_or_paths(paths) - command_str = self._create_command_str() - cancel = self._print_info(selection, command_str, open_args) + command_str = self._command_str() - if not cancel: - play(command_str, paths, open_args, keep_open=True) + if not self._exceeds_threshold(selection, command_str, open_args): + play(command_str, selection, paths, open_args, self._log, + keep_open=True) diff --git a/docs/plugins/play.rst b/docs/plugins/play.rst index 6a34cda57..9b9110bde 100644 --- a/docs/plugins/play.rst +++ b/docs/plugins/play.rst @@ -36,7 +36,7 @@ The `play` plugin can also be invoked during an import. If enabled, the plugin adds a `plaY` option to the prompt, so pressing `y` will execute the configured command and play the items currently being imported. -Once you exit your configured player, you will be returned to the import +Once the configured command exits, you will be returned to the import decision prompt. If your player is configured to run in the background (in a client/server setup), the music will play until you choose to stop it, and the import operation continues immediately. From efbd58cd556999617736e09e0a58cfd61c5730ec Mon Sep 17 00:00:00 2001 From: Marvin Steadfast Date: Fri, 6 Jan 2017 11:44:32 +0100 Subject: [PATCH 034/193] embyupdate: fix bug that config for password and api is needed even if it only used api key it needed the password key. this is fixed now. --- beetsplug/embyupdate.py | 10 +++++++++- docs/changelog.rst | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/beetsplug/embyupdate.py b/beetsplug/embyupdate.py index 3af285973..6f74b4452 100644 --- a/beetsplug/embyupdate.py +++ b/beetsplug/embyupdate.py @@ -6,6 +6,7 @@ host: localhost port: 8096 username: user + apikey: apikey password: password """ from __future__ import division, absolute_import, print_function @@ -150,7 +151,9 @@ class EmbyUpdate(BeetsPlugin): # Adding defaults. config['emby'].add({ u'host': u'http://localhost', - u'port': 8096 + u'port': 8096, + u'apikey': None, + u'password': None }) self.register_listener('database_change', self.listen_for_db_change) @@ -171,6 +174,11 @@ class EmbyUpdate(BeetsPlugin): password = config['emby']['password'].get() token = config['emby']['apikey'].get() + # Check if at least a apikey or password is given. + if not any([password, token]): + self._log.warning(u'Provide at least Emby password or apikey.') + return + # Get user information from the Emby API. user = get_user(host, port, username) if not user: diff --git a/docs/changelog.rst b/docs/changelog.rst index 102154bbc..5a3451017 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -44,6 +44,7 @@ Fixes: :bug:`2302` * :doc:`/plugins/lyrics`: The plugin now reports a beets-specific User-Agent header when requesting lyrics. :bug:`2357` +* :doc:`/plugins/embyupdate`: Fix a bug that apikey and password is needed in config. For plugin developers: new importer prompt choices (see :ref:`append_prompt_choices`), you can now provide new candidates for the user to consider. From 8d613425fd6b4125399f18fe701b007738c85645 Mon Sep 17 00:00:00 2001 From: diomekes Date: Fri, 6 Jan 2017 23:46:16 -0500 Subject: [PATCH 035/193] small docstring rewrite --- beetsplug/play.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/beetsplug/play.py b/beetsplug/play.py index 032894777..af658114b 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -158,9 +158,8 @@ class PlayPlugin(BeetsPlugin): def _exceeds_threshold(self, selection, command_str, open_args, item_type='track'): - """Prompt user whether to continue if playlist exceeds threshold. If - this returns True, the tracks or albums are not played, if False, - the play command is run. + """Prompt user whether to abort if playlist exceeds threshold. If + True, cancel playback. If False, execute play command. """ warning_threshold = config['play']['warning_threshold'].get(int) # We use -2 as a default value for warning_threshold to detect if it is From 5ee89666b6b8e2a8f3be1a004e33d8b309e90fb5 Mon Sep 17 00:00:00 2001 From: diomekes Date: Sat, 7 Jan 2017 09:54:54 -0500 Subject: [PATCH 036/193] fix typo in config docs --- docs/reference/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 8f009fc3f..8e43e2dd7 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -229,7 +229,7 @@ Default sort order to use when fetching items from the database. Defaults to sort_album ~~~~~~~~~~ -Default sort order to use when fetching items from the database. Defaults to +Default sort order to use when fetching albums from the database. Defaults to ``albumartist+ album+``. Explicit sort orders override this default. .. _sort_case_insensitive: From d992221853e7b332da9cdd2c06c648b9b78dca9e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Jan 2017 17:03:30 -0500 Subject: [PATCH 037/193] Changelog for #2360 (fix #2008) --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5a3451017..28605fba2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,9 @@ Features: :bug:`2349` * :doc:`/plugins/bpm`: Now uses the ``import.write`` configuration option to decide whether or not to write tracks after updating their BPM. :bug:`1992` +* :doc:`/plugins/play`: The plugin now provides an importer prompt choice to + play the music you're about to import. Thanks to :user:`diomekes`. + :bug:`2008` :bug:`2360` Fixes: From c9ec5e411ca67880224f6491fed9548f58aba1e8 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Jan 2017 17:07:01 -0500 Subject: [PATCH 038/193] A little fiddling with embyupdate Clean up some wording w/r/t efbd58cd556999617736e09e0a58cfd61c5730ec. --- beetsplug/embyupdate.py | 2 +- docs/changelog.rst | 3 ++- docs/plugins/embyupdate.rst | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/beetsplug/embyupdate.py b/beetsplug/embyupdate.py index 6f74b4452..5c731954b 100644 --- a/beetsplug/embyupdate.py +++ b/beetsplug/embyupdate.py @@ -153,7 +153,7 @@ class EmbyUpdate(BeetsPlugin): u'host': u'http://localhost', u'port': 8096, u'apikey': None, - u'password': None + u'password': None, }) self.register_listener('database_change', self.listen_for_db_change) diff --git a/docs/changelog.rst b/docs/changelog.rst index 28605fba2..69ee77e8c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -47,7 +47,8 @@ Fixes: :bug:`2302` * :doc:`/plugins/lyrics`: The plugin now reports a beets-specific User-Agent header when requesting lyrics. :bug:`2357` -* :doc:`/plugins/embyupdate`: Fix a bug that apikey and password is needed in config. +* :doc:`/plugins/embyupdate`: The plugin now checks whether an API key or a + password is provided in the configuration. For plugin developers: new importer prompt choices (see :ref:`append_prompt_choices`), you can now provide new candidates for the user to consider. diff --git a/docs/plugins/embyupdate.rst b/docs/plugins/embyupdate.rst index 8dbb7c1db..00373b98c 100644 --- a/docs/plugins/embyupdate.rst +++ b/docs/plugins/embyupdate.rst @@ -3,7 +3,7 @@ EmbyUpdate Plugin ``embyupdate`` is a plugin that lets you automatically update `Emby`_'s library whenever you change your beets library. -To use ``embyupdate`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then, you'll probably want to configure the specifics of your Emby server. You can do that using an ``emby:`` section in your ``config.yaml``, which looks like this:: +To use ``embyupdate`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then, you'll want to configure the specifics of your Emby server. You can do that using an ``emby:`` section in your ``config.yaml``, which looks like this:: emby: host: localhost From 6b9d766082ad1f0a96d22430c3659a9c182f7f67 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Jan 2017 17:09:51 -0500 Subject: [PATCH 039/193] Remove compatibility with misspelled config option This has been hanging around long enough; it's about time to drop the old name. --- beetsplug/play.py | 14 +------------- docs/changelog.rst | 2 ++ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/beetsplug/play.py b/beetsplug/play.py index af658114b..636d98d46 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -64,9 +64,7 @@ class PlayPlugin(BeetsPlugin): 'use_folders': False, 'relative_to': None, 'raw': False, - # Backwards compatibility. See #1803 and line 155 - 'warning_threshold': -2, - 'warning_treshold': 100, + 'warning_threshold': 100, }) self.register_listener('before_choose_candidate', @@ -162,16 +160,6 @@ class PlayPlugin(BeetsPlugin): True, cancel playback. If False, execute play command. """ warning_threshold = config['play']['warning_threshold'].get(int) - # We use -2 as a default value for warning_threshold to detect if it is - # set or not. We can't use a falsey value because it would have an - # actual meaning in the configuration of this plugin, and we do not use - # -1 because some people might use it as a value to obtain no warning, - # which wouldn't be that bad of a practice. - if warning_threshold == -2: - # if warning_threshold has not been set by user, look for - # warning_treshold, to preserve backwards compatibility. See #1803. - # warning_treshold has the correct default value of 100. - warning_threshold = config['play']['warning_treshold'].get(int) # Warn user before playing any huge playlists. if warning_threshold and len(selection) > warning_threshold: diff --git a/docs/changelog.rst b/docs/changelog.rst index 69ee77e8c..68b3e12e3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -49,6 +49,8 @@ Fixes: header when requesting lyrics. :bug:`2357` * :doc:`/plugins/embyupdate`: The plugin now checks whether an API key or a password is provided in the configuration. +* :doc:`/plugins/play`: The misspelled configuration option + ``warning_treshold`` is no longer supported. For plugin developers: new importer prompt choices (see :ref:`append_prompt_choices`), you can now provide new candidates for the user to consider. From 0b5b20d7990d2aec879e7c03cb4c6f8257054435 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Jan 2017 17:15:13 -0500 Subject: [PATCH 040/193] Robust import of distutils submodule (fix #2376) This would fail if the `spawn` module in the `distutils` package was not already imported somewhere else. --- beetsplug/absubmit.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/beetsplug/absubmit.py b/beetsplug/absubmit.py index ae3717470..e96d4f033 100644 --- a/beetsplug/absubmit.py +++ b/beetsplug/absubmit.py @@ -24,7 +24,7 @@ import os import subprocess import tempfile -import distutils +from distutils.spawn import find_executable import requests from beets import plugins @@ -79,9 +79,10 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): # Extractor found, will exit with an error if not called with # the correct amount of arguments. pass - # Get the executable location on the system, - # needed to calculate the sha1 hash. - self.extractor = distutils.spawn.find_executable(self.extractor) + + # Get the executable location on the system, which we need + # to calculate the SHA-1 hash. + self.extractor = find_executable(self.extractor) # Calculate extractor hash. self.extractor_sha = hashlib.sha1() From e5e710033cfb971e76fc03fca7a32e818b94e514 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Jan 2017 17:19:02 -0500 Subject: [PATCH 041/193] Remove old test for misspelled config option See 6b9d766, which removed the option. --- test/test_play.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/test_play.py b/test/test_play.py index 9d66d7466..86fef99a9 100644 --- a/test/test_play.py +++ b/test/test_play.py @@ -115,15 +115,6 @@ class PlayPluginTest(unittest.TestCase, TestHelper): open_mock.assert_not_called() - def test_warning_threshold_backwards_compat(self, open_mock): - self.config['play']['warning_treshold'] = 1 - self.add_item(title=u'another NiceTitle') - - with control_stdin("a"): - self.run_command(u'play', u'nice') - - open_mock.assert_not_called() - def test_command_failed(self, open_mock): open_mock.side_effect = OSError(u"some reason") From 2cef4703f8f9c20e872404e62b9df8ca39f91968 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 9 Jan 2017 10:37:37 -0500 Subject: [PATCH 042/193] Prepare 1.4.3 changelog for release --- docs/changelog.rst | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 68b3e12e3..6910643dc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,15 @@ Changelog 1.4.3 (in development) ---------------------- -Features: +Happy new year! This new version includes a cornucopia of new features from +contributors, including new tags related to classical music and a new +:doc:`/plugins/absubmit` for performing acoustic analysis on your music. The +:doc:`/plugins/random` has a new mode that lets you generate time-limited +music---for example, you might generate a random playlist that lasts the +perfect length for your walk to work. We also access as many Web services as +possible over secure connections now---HTTPS everywhere! + +The most visible new features are: * We now support the composer, lyricist, and arranger tags. The MusicBrainz data source will fetch data for these fields when the next version of @@ -13,26 +21,29 @@ Features: * A new :doc:`/plugins/absubmit` lets you run acoustic analysis software and upload the results for others to use. Thanks to :user:`inytar`. :bug:`2253` :bug:`2342` +* :doc:`/plugins/play`: The plugin now provides an importer prompt choice to + play the music you're about to import. Thanks to :user:`diomekes`. + :bug:`2008` :bug:`2360` +* We now use SSL to access Web services whenever possible. That includes + MusicBrainz itself, several album art sources, some lyrics sources, and + other servers. Thanks to :user:`tigranl`. :bug:`2307` * :doc:`/plugins/random`: A new ``--time`` option lets you generate a random playlist that takes a given amount of time. Thanks to :user:`diomekes`. :bug:`2305` :bug:`2322` -* :doc:`/plugins/zero`: Added ``zero`` command to manually trigger the zero + +Some smaller new features: + +* :doc:`/plugins/zero`: A new ``zero`` command manually triggers the zero plugin. Thanks to :user:`SJoshBrown`. :bug:`2274` :bug:`2329` * :doc:`/plugins/acousticbrainz`: The plugin will avoid re-downloading data for files that already have it by default. You can override this behavior using a new ``force`` option. Thanks to :user:`SusannaMaria`. :bug:`2347` :bug:`2349` -* :doc:`/plugins/bpm`: Now uses the ``import.write`` configuration option to - decide whether or not to write tracks after updating their BPM. :bug:`1992` -* :doc:`/plugins/play`: The plugin now provides an importer prompt choice to - play the music you're about to import. Thanks to :user:`diomekes`. - :bug:`2008` :bug:`2360` +* :doc:`/plugins/bpm`: The ``import.write`` configuration option now + decides whether or not to write tracks after updating their BPM. :bug:`1992` -Fixes: +And the fixes: -* We now use SSL to access Web services whenever possible. That includes - MusicBrainz itself, several album art sources, some lyrics sources, and - other servers. Thanks to :user:`tigranl`. :bug:`2307` * :doc:`/plugins/bpd`: Fix a crash on non-ASCII MPD commands. :bug:`2332` * :doc:`/plugins/scrub`: Avoid a crash when files cannot be read or written. :bug:`2351` @@ -42,8 +53,8 @@ Fixes: filesystem. :bug:`2353` * :doc:`/plugins/discogs`: Improve the handling of releases that contain subtracks. :bug:`2318` -* :doc:`/plugins/discogs`: Fix a crash when a release did not contain Format - information, and increased robustness when other fields are missing. +* :doc:`/plugins/discogs`: Fix a crash when a release does not contain format + information, and increase robustness when other fields are missing. :bug:`2302` * :doc:`/plugins/lyrics`: The plugin now reports a beets-specific User-Agent header when requesting lyrics. :bug:`2357` @@ -52,7 +63,11 @@ Fixes: * :doc:`/plugins/play`: The misspelled configuration option ``warning_treshold`` is no longer supported. -For plugin developers: new importer prompt choices (see :ref:`append_prompt_choices`), you can now provide new candidates for the user to consider. +For plugin developers: when providing new importer prompt choices (see +:ref:`append_prompt_choices`), you can now provide new candidates for the user +to consider. For example, you might provide an alternative strategy for +picking between the available alternatives or for looking up a release on +MusicBrainz. 1.4.2 (December 16, 2016) From 497c3076d2f9a4ed2221cb29b028f5f4fabe0671 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 9 Jan 2017 10:38:06 -0500 Subject: [PATCH 043/193] Add 1.4.3 release date --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6910643dc..354aae23e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,8 +1,8 @@ Changelog ========= -1.4.3 (in development) ----------------------- +1.4.3 (January 9, 2017) +----------------------- Happy new year! This new version includes a cornucopia of new features from contributors, including new tags related to classical music and a new From 0bd6062ee9dd53dee26fa29b5c08285fff711081 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 9 Jan 2017 10:40:06 -0500 Subject: [PATCH 044/193] Version bump: 1.4.4 --- beets/__init__.py | 2 +- docs/changelog.rst | 6 ++++++ docs/conf.py | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index 5d82b05f7..964d2592c 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -19,7 +19,7 @@ import os from beets.util import confit -__version__ = u'1.4.3' +__version__ = u'1.4.4' __author__ = u'Adrian Sampson ' diff --git a/docs/changelog.rst b/docs/changelog.rst index 354aae23e..f2c7d803c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog ========= +1.4.4 (in development) +---------------------- + +Changelog goes here! + + 1.4.3 (January 9, 2017) ----------------------- diff --git a/docs/conf.py b/docs/conf.py index 99771e7b1..9573b2fba 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,7 @@ project = u'beets' copyright = u'2016, Adrian Sampson' version = '1.4' -release = '1.4.3' +release = '1.4.4' pygments_style = 'sphinx' diff --git a/setup.py b/setup.py index f88fe28b6..35131cb55 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if 'sdist' in sys.argv: setup( name='beets', - version='1.4.3', + version='1.4.4', description='music tagger and library organizer', author='Adrian Sampson', author_email='adrian@radbox.org', From b9500c379ee1a18dfa3bfc2d41579d200c2c9dea Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 9 Jan 2017 12:44:53 -0500 Subject: [PATCH 045/193] Remove some unnecessary imports in mediafile.py --- beets/mediafile.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 0f595a36a..9e3f81db3 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -36,15 +36,11 @@ data from the tags. In turn ``MediaField`` uses a number of from __future__ import division, absolute_import, print_function import mutagen -import mutagen.mp3 import mutagen.id3 -import mutagen.oggopus -import mutagen.oggvorbis import mutagen.mp4 import mutagen.flac -import mutagen.monkeysaudio import mutagen.asf -import mutagen.aiff + import codecs import datetime import re From f7742939ef65767dc3fa98ca5a57e4eb53f8de6f Mon Sep 17 00:00:00 2001 From: Boris Pruessmann Date: Sun, 8 Jan 2017 19:47:59 +0100 Subject: [PATCH 046/193] Support for DSF files --- beets/mediafile.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 9e3f81db3..59959b8f5 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -40,7 +40,6 @@ import mutagen.id3 import mutagen.mp4 import mutagen.flac import mutagen.asf - import codecs import datetime import re @@ -73,6 +72,7 @@ TYPES = { 'mpc': 'Musepack', 'asf': 'Windows Media', 'aiff': 'AIFF', + 'dsf': 'DSD Stream File', } PREFERRED_IMAGE_EXTENSIONS = {'jpeg': 'jpg'} @@ -728,7 +728,7 @@ class MP4ImageStorageStyle(MP4ListStorageStyle): class MP3StorageStyle(StorageStyle): """Store data in ID3 frames. """ - formats = ['MP3', 'AIFF'] + formats = ['MP3', 'AIFF', 'DSF'] def __init__(self, key, id3_lang=None, **kwargs): """Create a new ID3 storage style. `id3_lang` is the value for @@ -1475,6 +1475,8 @@ class MediaFile(object): self.type = 'asf' elif type(self.mgfile).__name__ == 'AIFF': self.type = 'aiff' + elif type(self.mgfile).__name__ == 'DSF': + self.type = 'dsf' else: raise FileTypeError(path, type(self.mgfile).__name__) From 1e10e62d8294770445ce34017b203d87b1ccc113 Mon Sep 17 00:00:00 2001 From: Boris Pruessmann Date: Mon, 9 Jan 2017 18:32:11 +0100 Subject: [PATCH 047/193] Added DSF to test_mediafile --- test/rsrc/empty.dsf | Bin 0 -> 4188 bytes test/rsrc/full.dsf | Bin 0 -> 113475 bytes test/rsrc/unparseable.dsf | Bin 0 -> 4208 bytes test/test_mediafile.py | 12 ++++++++++++ 4 files changed, 12 insertions(+) create mode 100644 test/rsrc/empty.dsf create mode 100644 test/rsrc/full.dsf create mode 100644 test/rsrc/unparseable.dsf diff --git a/test/rsrc/empty.dsf b/test/rsrc/empty.dsf new file mode 100644 index 0000000000000000000000000000000000000000..4cbceb3c97977747977423c92b2e39e7f7a7d192 GIT binary patch literal 4188 zcmZ<>c2SUFfPfeQC<98R<(4RzK-r8)G?*=@%>WYvOF{LfB$g!dpqT^JI!cX(z-S1J lhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2n@pz003D!1tkCg literal 0 HcmV?d00001 diff --git a/test/rsrc/full.dsf b/test/rsrc/full.dsf new file mode 100644 index 0000000000000000000000000000000000000000..a90e6946fc44cd97a56c9846cbea855201a22808 GIT binary patch literal 113475 zcmeIuPj3@P7zf}jX)jPjx%XgkU*dlo@4|`LAVnp`BAY6Q_AqO&jV0TOZ07(cz82!E z@L`yZ9SOcc56?*6o%bEjZ)Ts-#i;dXvLC~*zuZm!{jz`0R=ZaJu(t0c$%9xozvS6J zNz?oMttsO8WB;0lUHJX5f8X&WK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+0D=FTz}cwxYjdZg z|H`wn8z;ZT`RsPNEXS`qv3wNE-G{mrHoL`kSH60C8k-OAo6G8URlXno6?-w|sC(Qi z$L}rS&d$U;2%i+7`FZb=q_3Gw& zTW@0f@7TUScwVnJbvZmAAJRJI`0~e#Ekji=V^S0KG@eke=SlMwF>Y0_ciYR0w`E*< z^25^d<7P40UcNaS#f6ti@?p2Td2xKay4@}&)g~;~pPpZD=1E@Vc{dwmX*&;D+V5xG zGz`KZom5pGI$5tXE9&y<>S`Zyblz+~z1!Y;bw}7bnc2SUFfPex42pt2Z({f7`OrUa%NHmx&sLcQq14}{mq$HLk@<7>8Eu++E2#kin vXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQKfm0Z$iW7O?lY5S|18{w@Y{ literal 0 HcmV?d00001 diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 5fcb14187..2adb57c85 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -887,6 +887,18 @@ class AIFFTest(ReadWriteTestBase, unittest.TestCase): } +class DSFTest(ReadWriteTestBase, unittest.TestCase): + extension = 'dsf' + audio_properties = { + 'length': 0.01, + 'bitrate': 11289600, + 'format': u'DSD Stream File', + 'samplerate': 5644800, + 'bitdepth': 1, + 'channels': 2, + } + + class MediaFieldTest(unittest.TestCase): def test_properties_from_fields(self): From a2d37dd5886dc23f652a13d7faaba4301d8451c4 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 9 Jan 2017 13:02:28 -0500 Subject: [PATCH 048/193] Document how to add tests for a new format (#2379) --- test/test_mediafile.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 5fcb14187..a948fe31f 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -294,8 +294,20 @@ class GenreListTestMixin(object): class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, _common.TempDirMixin): - """Test writing and reading tags. Subclasses must set ``extension`` and - ``audio_properties``. + """Test writing and reading tags. Subclasses must set ``extension`` + and ``audio_properties``. + + The basic tests for all audio formats encompass three files provided + in our `rsrc` folder: `full.*`, `empty.*`, and `unparseable.*`. + Respectively, they should contain a full slate of common fields + listed in `full_initial_tags` below; no fields contents at all; and + an unparseable release date field. + + To add support for a new file format to MediaFile, add these three + files and then create a `ReadWriteTestBase` subclass by copying n' + pasting one of the existing subclasses below. You will want to + update the `format` field in that subclass, and you will probably + need to fiddle with the `bitrate` and other format-specific fields. """ full_initial_tags = { @@ -554,6 +566,9 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, self.assertEqual(mediafile.disctotal, None) def test_unparseable_date(self): + """The `unparseable.*` fixture should not crash but should return None + for all parts of the release date. + """ mediafile = self._mediafile_fixture('unparseable') self.assertIsNone(mediafile.date) From e0a4dc67a85be25156f8012be83faff2cf60cb9a Mon Sep 17 00:00:00 2001 From: Boris Pruessmann Date: Tue, 10 Jan 2017 11:06:57 +0100 Subject: [PATCH 049/193] Test improvements for DSF. - Fixed unparseable.dsf - Added DSF feature detection to test_mediafile.py --- test/rsrc/unparseable.dsf | Bin 4208 -> 5243 bytes test/test_mediafile.py | 10 +++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/test/rsrc/unparseable.dsf b/test/rsrc/unparseable.dsf index c5463d5c181ca3261180d4fdedde9888bbc0c325..3b6292e32df6494bca8782c8795627babe575f50 100644 GIT binary patch delta 52 zcmeyM@LPk=CD=tlh5-VqMK Date: Tue, 10 Jan 2017 12:22:30 -0500 Subject: [PATCH 050/193] Fix #2381: mpdupdate on Python 3 Communicate bytes over the socket, obvi. --- beetsplug/mpdupdate.py | 22 +++++++++++----------- docs/changelog.rst | 4 +++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/beetsplug/mpdupdate.py b/beetsplug/mpdupdate.py index 6c39375be..6ecc92131 100644 --- a/beetsplug/mpdupdate.py +++ b/beetsplug/mpdupdate.py @@ -35,14 +35,14 @@ import six # easier. class BufferedSocket(object): """Socket abstraction that allows reading by line.""" - def __init__(self, host, port, sep='\n'): + def __init__(self, host, port, sep=b'\n'): if host[0] in ['/', '~']: self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(os.path.expanduser(host)) else: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((host, port)) - self.buf = '' + self.buf = b'' self.sep = sep def readline(self): @@ -51,11 +51,11 @@ class BufferedSocket(object): if not data: break self.buf += data - if '\n' in self.buf: + if self.sep in self.buf: res, self.buf = self.buf.split(self.sep, 1) return res + self.sep else: - return '' + return b'' def send(self, data): self.sock.send(data) @@ -106,24 +106,24 @@ class MPDUpdatePlugin(BeetsPlugin): return resp = s.readline() - if 'OK MPD' not in resp: + if b'OK MPD' not in resp: self._log.warning(u'MPD connection failed: {0!r}', resp) return if password: - s.send('password "%s"\n' % password) + s.send(b'password "%s"\n' % password.encode('utf8')) resp = s.readline() - if 'OK' not in resp: + if b'OK' not in resp: self._log.warning(u'Authentication failed: {0!r}', resp) - s.send('close\n') + s.send(b'close\n') s.close() return - s.send('update\n') + s.send(b'update\n') resp = s.readline() - if 'updating_db' not in resp: + if b'updating_db' not in resp: self._log.warning(u'Update failed: {0!r}', resp) - s.send('close\n') + s.send(b'close\n') s.close() self._log.info(u'Database updated.') diff --git a/docs/changelog.rst b/docs/changelog.rst index f2c7d803c..8bce7c62a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,9 @@ Changelog 1.4.4 (in development) ---------------------- -Changelog goes here! +Fixes: + +* :doc:`/plugins/mpdupdate`: Fix Python 3 compatibility. :bug:`2381` 1.4.3 (January 9, 2017) From da89be81fcb526a731650218677d2607a5aa9111 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 10 Jan 2017 12:30:48 -0500 Subject: [PATCH 051/193] Changelog for #2379 --- beets/mediafile.py | 1 + docs/changelog.rst | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/beets/mediafile.py b/beets/mediafile.py index 59959b8f5..c910c9a86 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -40,6 +40,7 @@ import mutagen.id3 import mutagen.mp4 import mutagen.flac import mutagen.asf + import codecs import datetime import re diff --git a/docs/changelog.rst b/docs/changelog.rst index 8bce7c62a..a6de16f9c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,11 @@ Changelog 1.4.4 (in development) ---------------------- +New features: + +* Added support for DSF files, once a future version of Mutagen is released + that supports them. Thanks to :user:`docbobo`. :bug:`459` :bug:`2379` + Fixes: * :doc:`/plugins/mpdupdate`: Fix Python 3 compatibility. :bug:`2381` From 5e20cfd26e3d9d25ae9fca1cd4ae81a0ccc928ed Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 10 Jan 2017 12:33:23 -0500 Subject: [PATCH 052/193] flake8 fixes for 2379 --- test/test_mediafile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 1bb30f560..29f1efa9f 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -901,6 +901,7 @@ class AIFFTest(ReadWriteTestBase, unittest.TestCase): 'channels': 1, } + try: import mutagen.dsf except: @@ -908,6 +909,7 @@ except: else: HAVE_DSF = True + @unittest.skipIf(not HAVE_DSF, "mutagen < 1.37") class DSFTest(ReadWriteTestBase, ExtendedImageStructureTestMixin, unittest.TestCase): From 5863859a5d5e2259078d0077940083a70a9d213d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 10 Jan 2017 12:38:44 -0500 Subject: [PATCH 053/193] Remove image tests for DSF (#2379) There isn't currently an `image.dsf`, so those tests fail. --- test/test_mediafile.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 29f1efa9f..133883942 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -910,9 +910,8 @@ else: HAVE_DSF = True -@unittest.skipIf(not HAVE_DSF, "mutagen < 1.37") -class DSFTest(ReadWriteTestBase, - ExtendedImageStructureTestMixin, unittest.TestCase): +@unittest.skipIf(not HAVE_DSF, "Mutagen does not have DSF support") +class DSFTest(ReadWriteTestBase, unittest.TestCase): extension = 'dsf' audio_properties = { 'length': 0.01, From f137f878781a3cd9af82f1700be854724b107792 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 10 Jan 2017 12:41:34 -0500 Subject: [PATCH 054/193] More test docs about the image.* mixin --- test/test_mediafile.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 133883942..e003e5dcc 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -175,7 +175,11 @@ class ImageStructureTestMixin(ArtTestMixin): class ExtendedImageStructureTestMixin(ImageStructureTestMixin): - """Checks for additional attributes in the image structure.""" + """Checks for additional attributes in the image structure. + + Like the base `ImageStructureTestMixin`, per-format test classes + should include this mixin to add image-related tests. + """ def assertExtendedImageAttributes(self, image, desc=None, type=None): # noqa self.assertEqual(image.desc, desc) @@ -308,6 +312,9 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, pasting one of the existing subclasses below. You will want to update the `format` field in that subclass, and you will probably need to fiddle with the `bitrate` and other format-specific fields. + + You can also add image tests (using an additional `image.*` fixture + file) by including one of the image-related mixins. """ full_initial_tags = { From f7ebf5524f3107e230707b2c49c8a64cc9ab5d1b Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 10 Jan 2017 13:24:33 -0500 Subject: [PATCH 055/193] flake8 fix for #2379 --- test/test_mediafile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index e003e5dcc..63df38b8e 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -909,8 +909,10 @@ class AIFFTest(ReadWriteTestBase, unittest.TestCase): } +# Check whether we have a Mutagen version with DSF support. We can +# remove this once we require a version that includes the feature. try: - import mutagen.dsf + import mutagen.dsf # noqa except: HAVE_DSF = False else: From 153b01e5a6e981d1939296a6061319a35c20a443 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 10 Jan 2017 14:24:18 -0500 Subject: [PATCH 056/193] replaygain: Don't muck with logging level This is now handled by the central logging infrastructure; no need to change it here. I think this must be a leftover from the era when plugins had to explicitly muck with their verbosity level, but even still it doesn't make sense to do set the level to INFO unconditionally... --- beetsplug/replaygain.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 2d3af58a6..c2376ce60 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -794,7 +794,7 @@ class ReplayGainPlugin(BeetsPlugin): "command": CommandBackend, "gstreamer": GStreamerBackend, "audiotools": AudioToolsBackend, - "bs1770gain": Bs1770gainBackend + "bs1770gain": Bs1770gainBackend, } def __init__(self): @@ -934,8 +934,6 @@ class ReplayGainPlugin(BeetsPlugin): """Return the "replaygain" ui subcommand. """ def func(lib, opts, args): - self._log.setLevel(logging.INFO) - write = ui.should_write() if opts.album: From bc93a11141c5154a8aee5c59107eec54acf084f8 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 10 Jan 2017 14:45:57 -0500 Subject: [PATCH 057/193] Fix #2382: replaygain backend parsing on Python 3 --- beetsplug/replaygain.py | 6 +++--- docs/changelog.rst | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index c2376ce60..eaa01d3a9 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -194,8 +194,8 @@ class Bs1770gainBackend(Backend): """ # Construct shell command. cmd = [self.command] - cmd = cmd + [self.method] - cmd = cmd + ['-p'] + cmd += [self.method] + cmd += ['-p'] # Workaround for Windows: the underlying tool fails on paths # with the \\?\ prefix, so we don't use it here. This @@ -227,7 +227,7 @@ class Bs1770gainBackend(Backend): ':|done\\.\\s)', re.DOTALL | re.UNICODE) results = re.findall(regex, data) for parts in results[0:num_lines]: - part = parts.split(b'\n') + part = parts.split(u'\n') if len(part) == 0: self._log.debug(u'bad tool output: {0!r}', text) raise ReplayGainError(u'bs1770gain failed') diff --git a/docs/changelog.rst b/docs/changelog.rst index a6de16f9c..4e0d7405f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,8 @@ New features: Fixes: * :doc:`/plugins/mpdupdate`: Fix Python 3 compatibility. :bug:`2381` +* :doc:`/plugins/replaygain`: Fix Python 3 compatibility in the ``bs1770gain`` + backend. :bug:`2382` 1.4.3 (January 9, 2017) From 998e6ac1c77ce4f270915863bbbbfffdce589b36 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 10 Jan 2017 14:54:17 -0500 Subject: [PATCH 058/193] Remove unused import --- beetsplug/replaygain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index eaa01d3a9..4cf7da7c5 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -23,7 +23,6 @@ import warnings import re from six.moves import zip -from beets import logging from beets import ui from beets.plugins import BeetsPlugin from beets.util import syspath, command_output, displayable_path, py3_path From 3f67a27989d264fe96005b96eaf86e3750547f3d Mon Sep 17 00:00:00 2001 From: Mike Cameron Date: Tue, 10 Jan 2017 18:21:28 -0500 Subject: [PATCH 059/193] Fixed failing test because mocked data was missing property umber. --- test/test_importer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_importer.py b/test/test_importer.py index 500ca027d..87724f8da 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -1723,6 +1723,7 @@ def mocked_get_release_by_id(id_, includes=[], release_status=[], 'length': 59, }, 'position': 9, + 'number': 'A2' }], 'position': 5, }], From c58c49d77ff484965215f07975f9f410ad8b6777 Mon Sep 17 00:00:00 2001 From: Mike Cameron Date: Tue, 10 Jan 2017 18:39:01 -0500 Subject: [PATCH 060/193] Fixed trailing whitespace issue. Changed alternate track property name. --- beets/autotag/__init__.py | 6 +++--- beets/autotag/hooks.py | 7 ++++--- beets/autotag/mb.py | 6 +++--- beets/library.py | 8 ++++---- beetsplug/discogs.py | 6 +++--- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index eb3dae519..7487477e5 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -34,7 +34,7 @@ log = logging.getLogger('beets') def apply_item_metadata(item, track_info): """Set an item's metadata from its matched TrackInfo object. - """ + """ item.artist = track_info.artist item.artist_sort = track_info.artist_sort item.artist_credit = track_info.artist_credit @@ -157,5 +157,5 @@ def apply_metadata(album_info, mapping): item.composer = track_info.composer if track_info.arranger is not None: item.arranger = track_info.arranger - - item.alt_track_no = track_info.alt_track_no \ No newline at end of file + + item.track_alt = track_info.track_alt \ No newline at end of file diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index a9a35e635..3c403fcf4 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -145,6 +145,7 @@ class TrackInfo(object): - ``lyricist``: individual track lyricist name - ``composer``: individual track composer name - ``arranger`: individual track arranger name + - ``track_alt``: alternative track number (tape, vinyl, etc.) Only ``title`` and ``track_id`` are required. The rest of the fields may be None. The indices ``index``, ``medium``, and ``medium_index`` @@ -154,8 +155,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, - alt_track_no=None): + media=None, lyricist=None, composer=None, arranger=None, + track_alt=None): self.title = title self.track_id = track_id self.artist = artist @@ -174,7 +175,7 @@ class TrackInfo(object): self.lyricist = lyricist self.composer = composer self.arranger = arranger - self.alt_track_no = alt_track_no + self.track_alt = track_alt # As above, work around a bug in python-musicbrainz-ngs. def decode(self, codec='utf-8'): diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index fc3b1926c..a0d4dc33a 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -256,8 +256,8 @@ def album_info(release): all_tracks = medium['track-list'] if 'pregap' in medium: all_tracks.insert(0, medium['pregap']) - - for track in all_tracks: + + for track in all_tracks: # Basic information from the recording. index += 1 ti = track_info( @@ -269,7 +269,7 @@ def album_info(release): ) ti.disctitle = disctitle ti.media = format - ti.alt_track_no = track['number'] + ti.track_alt = track['number'] # Prefer track data, where present, over recording data. if track.get('title'): diff --git a/beets/library.py b/beets/library.py index d9ffd38fd..99a84cee6 100644 --- a/beets/library.py +++ b/beets/library.py @@ -322,7 +322,7 @@ class LibModel(dbcore.Model): funcs.update(plugins.template_funcs()) return funcs - def store(self, fields=None): + def store(self, fields=None): super(LibModel, self).store(fields) plugins.send('database_change', lib=self._db, model=self) @@ -423,10 +423,10 @@ class Item(LibModel): 'month': types.PaddedInt(2), 'day': types.PaddedInt(2), 'track': types.PaddedInt(2), - 'alt_track_no': types.STRING, + 'track_alt': types.STRING, 'tracktotal': types.PaddedInt(2), 'disc': types.PaddedInt(2), - 'disctotal': types.PaddedInt(2), + 'disctotal': types.PaddedInt(2), 'lyrics': types.STRING, 'comments': types.STRING, 'bpm': types.INTEGER, @@ -457,7 +457,7 @@ class Item(LibModel): 'original_year': types.PaddedInt(4), 'original_month': types.PaddedInt(2), 'original_day': types.PaddedInt(2), - 'initial_key': MusicalKey(), + 'initial_key': MusicalKey(), 'length': DurationType(), 'bitrate': types.ScaledInt(1000, u'kbps'), diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 905358024..3b3f5f201 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -314,9 +314,9 @@ class DiscogsPlugin(BeetsPlugin): # Only real tracks have `position`. Otherwise, it's an index track. if track['position']: index += 1 - ti = self.get_track_info(track, index) - ti.alt_track_no = track['position'] - tracks.append(ti) + track_info = self.get_track_info(track, index) + track_info.track_alt = track['position'] + tracks.append(track_info) else: index_tracks[index + 1] = track['title'] From 3cd4f1c0910bd50bfaeb2e5aad38f22e6aad1a61 Mon Sep 17 00:00:00 2001 From: Mike Cameron Date: Tue, 10 Jan 2017 18:57:42 -0500 Subject: [PATCH 061/193] Fixed failing test where track number was missing from mocked data. --- test/test_mb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_mb.py b/test/test_mb.py index ecbcf3c59..9e4226307 100644 --- a/test/test_mb.py +++ b/test/test_mb.py @@ -68,6 +68,7 @@ class MBAlbumInfoTest(_common.TestCase): track = { 'recording': recording, 'position': i + 1, + 'number': 'A1', } if track_length: # Track lengths are distinct from recording lengths. @@ -182,12 +183,12 @@ class MBAlbumInfoTest(_common.TestCase): second_track_list = [{ 'recording': tracks[1], 'position': '1', + 'number': 'A1', }] release['medium-list'].append({ 'position': '2', 'track-list': second_track_list, }) - d = mb.album_info(release) self.assertEqual(d.mediums, 2) t = d.tracks @@ -454,6 +455,7 @@ class MBLibraryTest(unittest.TestCase): 'length': 42, }, 'position': 9, + 'number': 'A1', }], 'position': 5, }], From bba5a7c7126178802d1e205cccba4abff3fb6bec Mon Sep 17 00:00:00 2001 From: Mike Cameron Date: Tue, 10 Jan 2017 19:08:18 -0500 Subject: [PATCH 062/193] Fixed (?) failing test where umber was rack_alt was missing during import. --- test/test_edit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_edit.py b/test/test_edit.py index 2900d092a..0449f2333 100644 --- a/test/test_edit.py +++ b/test/test_edit.py @@ -276,7 +276,7 @@ class EditDuringImporterTest(TerminalImportSessionSetup, unittest.TestCase, ImportHelper, TestHelper, EditMixin): """TODO """ - IGNORED = ['added', 'album_id', 'id', 'mtime', 'path'] + IGNORED = ['added', 'album_id', 'id', 'mtime', 'path', 'track_alt'] def setUp(self): self.setup_beets() From 0dd763c5ef8e84351cd2a73d628a65fa4eb31638 Mon Sep 17 00:00:00 2001 From: Mike Cameron Date: Tue, 10 Jan 2017 19:19:09 -0500 Subject: [PATCH 063/193] Fixed flake8 warning W292 for Python 3.4 about missing newline at end of file. --- beets/autotag/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 7487477e5..822bb60ef 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -158,4 +158,4 @@ def apply_metadata(album_info, mapping): if track_info.arranger is not None: item.arranger = track_info.arranger - item.track_alt = track_info.track_alt \ No newline at end of file + item.track_alt = track_info.track_alt From 5250ba80202408bb58c00a7157c0331ab0e556b6 Mon Sep 17 00:00:00 2001 From: Mike Cameron Date: Tue, 10 Jan 2017 19:40:23 -0500 Subject: [PATCH 064/193] Added changelog entry of new rack_alt path template value and updated changelog with description of change. --- docs/changelog.rst | 3 +++ docs/reference/pathformat.rst | 1 + 2 files changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4e0d7405f..a9210554a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,9 @@ New features: * Added support for DSF files, once a future version of Mutagen is released that supports them. Thanks to :user:`docbobo`. :bug:`459` :bug:`2379` +* A new template path value ``track_alt`` let's you use the tracks actual + number rather than its position media. Useful when importing albums that + have multiple sides, such as tapes and vinyls. Fixes: diff --git a/docs/reference/pathformat.rst b/docs/reference/pathformat.rst index 72453a6e5..6b1fe5021 100644 --- a/docs/reference/pathformat.rst +++ b/docs/reference/pathformat.rst @@ -197,6 +197,7 @@ Ordinary metadata: * original_year, original_month, original_day: The release date of the original version of the album. * track +* track_alt * tracktotal * disc * disctotal From 703f47ae976ed4e95de4cb974d7785ab53b8cea5 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 10 Jan 2017 21:01:36 -0500 Subject: [PATCH 065/193] Use flexible attribute for track_alt No need for a built-in field for a simple string-type optional field like this. --- beets/library.py | 1 - docs/reference/pathformat.rst | 1 - test/test_edit.py | 2 +- test/test_mb.py | 1 + 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/beets/library.py b/beets/library.py index 99a84cee6..32176b68d 100644 --- a/beets/library.py +++ b/beets/library.py @@ -423,7 +423,6 @@ class Item(LibModel): 'month': types.PaddedInt(2), 'day': types.PaddedInt(2), 'track': types.PaddedInt(2), - 'track_alt': types.STRING, 'tracktotal': types.PaddedInt(2), 'disc': types.PaddedInt(2), 'disctotal': types.PaddedInt(2), diff --git a/docs/reference/pathformat.rst b/docs/reference/pathformat.rst index 6b1fe5021..72453a6e5 100644 --- a/docs/reference/pathformat.rst +++ b/docs/reference/pathformat.rst @@ -197,7 +197,6 @@ Ordinary metadata: * original_year, original_month, original_day: The release date of the original version of the album. * track -* track_alt * tracktotal * disc * disctotal diff --git a/test/test_edit.py b/test/test_edit.py index 0449f2333..2900d092a 100644 --- a/test/test_edit.py +++ b/test/test_edit.py @@ -276,7 +276,7 @@ class EditDuringImporterTest(TerminalImportSessionSetup, unittest.TestCase, ImportHelper, TestHelper, EditMixin): """TODO """ - IGNORED = ['added', 'album_id', 'id', 'mtime', 'path', 'track_alt'] + IGNORED = ['added', 'album_id', 'id', 'mtime', 'path'] def setUp(self): self.setup_beets() diff --git a/test/test_mb.py b/test/test_mb.py index 9e4226307..ca1bf2a1a 100644 --- a/test/test_mb.py +++ b/test/test_mb.py @@ -189,6 +189,7 @@ class MBAlbumInfoTest(_common.TestCase): 'position': '2', 'track-list': second_track_list, }) + d = mb.album_info(release) self.assertEqual(d.mediums, 2) t = d.tracks From da86a410fe77f7e37aa5f3527569bc48abb1326a Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 10 Jan 2017 21:04:51 -0500 Subject: [PATCH 066/193] A little more detail in the changelog --- docs/changelog.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a9210554a..eaf8c7d5a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,9 +8,11 @@ New features: * Added support for DSF files, once a future version of Mutagen is released that supports them. Thanks to :user:`docbobo`. :bug:`459` :bug:`2379` -* A new template path value ``track_alt`` let's you use the tracks actual - number rather than its position media. Useful when importing albums that - have multiple sides, such as tapes and vinyls. +* The MusicBrainz backend and :doc:`/plugins/discogs` now both provide a new + attribute called ``track_alt`` that stores more nuanced, possibly + non-numeric track index data. For example, some vinyl or tape media will + report the side of the record using a letter instead of a number in that + field. :bug:`1831` :bug:`2363` Fixes: From 272aa8870386b171c132206829509b410cd11d18 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 12 Jan 2017 13:16:35 -0500 Subject: [PATCH 067/193] Expand convert format config docs (fix #2384) --- docs/plugins/convert.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index f2f28c63d..b8069506d 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -87,7 +87,14 @@ file. The available options are: By default, the plugin will detect the number of processors available and use them all. -You can also configure the format to use for transcoding. +You can also configure the format to use for transcoding (see the next +section): + +- **format**: The name of the format to transcode to when none is specified on + the command line. + Default: ``mp3``. +- **formats**: A set of formats and associated command lines for transcoding + each. .. _convert-format-config: From 0f1a93c6666b90bd79e1390240c87ce9b22e2bd1 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 14 Jan 2017 17:33:36 -0800 Subject: [PATCH 068/193] Add missing unittest suite declaration (#2389) --- test/test_plugin_mediafield.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_plugin_mediafield.py b/test/test_plugin_mediafield.py index d9d2b0a1f..983f6e2c8 100644 --- a/test/test_plugin_mediafield.py +++ b/test/test_plugin_mediafield.py @@ -20,6 +20,7 @@ from __future__ import division, absolute_import, print_function import os import six import shutil +import unittest from test import _common from beets.library import Item @@ -105,3 +106,10 @@ class ExtendedFieldTestMixin(_common.TestCase): mediafile.MediaFile.add_field('artist', mediafile.MediaField()) self.assertIn(u'property "artist" already exists', six.text_type(cm.exception)) + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + +if __name__ == '__main__': + unittest.main(defaultTest='suite') From 31c49a62166a5ad073ca76c3b59c603540a3cea9 Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 11:28:42 -0800 Subject: [PATCH 069/193] beets.library.Library adds custom bytelower function to all connections, not just one --- beets/dbcore/db.py | 27 +++++++++++++++++---------- beets/library.py | 7 +++++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index d01e8a5c3..edd611928 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -733,19 +733,26 @@ class Database(object): if thread_id in self._connections: return self._connections[thread_id] else: - # Make a new connection. The `sqlite3` module can't use - # bytestring paths here on Python 3, so we need to - # provide a `str` using `py3_path`. - conn = sqlite3.connect( - py3_path(self.path), timeout=self.timeout - ) - - # Access SELECT results like dictionaries. - conn.row_factory = sqlite3.Row - + conn = self._create_connection() self._connections[thread_id] = conn return conn + def _create_connection(self): + """Create a SQLite connection to the underlying database. Makes + a new connection every time. If you need to add custom functions + to each connection, override this method. + """ + # Make a new connection. The `sqlite3` module can't use + # bytestring paths here on Python 3, so we need to + # provide a `str` using `py3_path`. + conn = sqlite3.connect( + py3_path(self.path), timeout=self.timeout + ) + + # Access SELECT results like dictionaries. + conn.row_factory = sqlite3.Row + return conn + def _close(self): """Close the all connections to the underlying SQLite database from all threads. This does not render the database object diff --git a/beets/library.py b/beets/library.py index 32176b68d..e3ac1bd40 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1237,14 +1237,17 @@ class Library(dbcore.Database): timeout = beets.config['timeout'].as_number() super(Library, self).__init__(path, timeout=timeout) - self._connection().create_function('bytelower', 1, _sqlite_bytelower) - self.directory = bytestring_path(normpath(directory)) self.path_formats = path_formats self.replacements = replacements self._memotable = {} # Used for template substitution performance. + def _create_connection(self): + conn = super(Library, self)._create_connection() + conn.create_function('bytelower', 1, _sqlite_bytelower) + return conn + # Adding objects to the database. def add(self, obj): From dd7f8a3a6fa754821dd1aa76e7a7e47a6da4d415 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 15 Jan 2017 10:12:49 -0800 Subject: [PATCH 070/193] Tiny docstring tweak for #2390 --- beets/dbcore/db.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index edd611928..6b0ed8b43 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -738,9 +738,11 @@ class Database(object): return conn def _create_connection(self): - """Create a SQLite connection to the underlying database. Makes - a new connection every time. If you need to add custom functions - to each connection, override this method. + """Create a SQLite connection to the underlying database. + + Makes a new connection every time. If you need to configure the + connection settings (e.g., add custom functions), override this + method. """ # Make a new connection. The `sqlite3` module can't use # bytestring paths here on Python 3, so we need to From 29d61ca634aeccbe2c8f007c8cfeed4001cdeeff Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 00:11:56 -0800 Subject: [PATCH 071/193] web.exclude_paths_from_items option More exclude_paths_from_items --- beetsplug/web/__init__.py | 9 ++++++++- docs/plugins/web.rst | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index e7b9ec81f..05872f6d2 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -37,7 +37,10 @@ def _rep(obj, expand=False): out = dict(obj) if isinstance(obj, beets.library.Item): - del out['path'] + if app.config['exclude_paths_from_items']: + del out['path'] + else: + out['path'] = out['path'].decode('utf-8') # Get the size (in bytes) of the backing file. This is useful # for the Tomahawk resolver API. @@ -309,6 +312,7 @@ class WebPlugin(BeetsPlugin): 'host': u'127.0.0.1', 'port': 8337, 'cors': '', + 'exclude_paths_from_items': True, }) def commands(self): @@ -327,6 +331,9 @@ class WebPlugin(BeetsPlugin): # Normalizes json output app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False + app.config['exclude_paths_from_items'] = ( + self.config['exclude_paths_from_items']) + # Enable CORS if required. if self.config['cors']: self._log.info(u'Enabling CORS with origin: {0}', diff --git a/docs/plugins/web.rst b/docs/plugins/web.rst index f4ae063e0..45000dc26 100644 --- a/docs/plugins/web.rst +++ b/docs/plugins/web.rst @@ -63,6 +63,8 @@ configuration file. The available options are: Default: 8337. - **cors**: The CORS allowed origin (see :ref:`web-cors`, below). Default: CORS is disabled. +- **exclude_paths_from_items**: The 'path' key of items is filtered out of JSON + responses for security reasons. Default: true. Implementation -------------- From 43936cd84c030e2588602c9eb99b47419ca905f8 Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 00:12:07 -0800 Subject: [PATCH 072/193] /item/at_path/ endpoint More at_path /item/by_path docs --- beetsplug/web/__init__.py | 8 ++++++++ docs/plugins/web.rst | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 05872f6d2..88bf299e4 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -221,6 +221,14 @@ def item_query(queries): return g.lib.items(queries) +@app.route('/item/at_path/') +def item_at_path(path): + try: + return flask.jsonify(_rep(beets.library.Item.from_path('/' + path))) + except beets.library.ReadError: + return flask.abort(404) + + @app.route('/item/values/') def item_unique_field_values(key): sort_key = flask.request.args.get('sort_key', key) diff --git a/docs/plugins/web.rst b/docs/plugins/web.rst index 45000dc26..93a67e7ea 100644 --- a/docs/plugins/web.rst +++ b/docs/plugins/web.rst @@ -162,6 +162,13 @@ response includes all the items requested. If a track is not found it is silentl dropped from the response. +``GET /item/by_path/...`` ++++++++++++++++++++++ + +Look for an item at the given path on the server. If it corresponds to a track, +return the track in the same format as /item/*. + + ``GET /item/query/querystring`` +++++++++++++++++++++++++++++++ From f6cb46d4900b4fe3e8d8453277bf5779e5589eeb Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 09:53:46 -0800 Subject: [PATCH 073/193] Fix broken tests (no new ones yet) --- beetsplug/web/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 88bf299e4..a981ccabc 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -340,7 +340,7 @@ class WebPlugin(BeetsPlugin): app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False app.config['exclude_paths_from_items'] = ( - self.config['exclude_paths_from_items']) + self.config.get('exclude_paths_from_items', True)) # Enable CORS if required. if self.config['cors']: From 50ea74635b365e0d20906572f829c9135c4c81e5 Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 10:08:39 -0800 Subject: [PATCH 074/193] Fix tests I broke --- 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 a981ccabc..1344b898d 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -37,7 +37,7 @@ def _rep(obj, expand=False): out = dict(obj) if isinstance(obj, beets.library.Item): - if app.config['exclude_paths_from_items']: + if app.config.get('EXCLUDE_PATHS_FROM_ITEMS', True): del out['path'] else: out['path'] = out['path'].decode('utf-8') @@ -339,7 +339,7 @@ class WebPlugin(BeetsPlugin): # Normalizes json output app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False - app.config['exclude_paths_from_items'] = ( + app.config['EXCLUDE_PATHS_FROM_ITEMS'] = ( self.config.get('exclude_paths_from_items', True)) # Enable CORS if required. From cedd93b7786a10308448c33bfd0191729ace03ef Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 10:15:41 -0800 Subject: [PATCH 075/193] Add tests for exclude_paths_from_items --- test/test_web.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/test_web.py b/test/test_web.py index e72ecf33d..d64341f3f 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -21,15 +21,24 @@ class WebPluginTest(_common.LibTestCase): # Add fixtures for track in self.lib.items(): track.remove() - self.lib.add(Item(title=u'title', path='', id=1)) - self.lib.add(Item(title=u'another title', path='', id=2)) + self.lib.add(Item(title=u'title', path='/path_1', id=1)) + self.lib.add(Item(title=u'another title', path='/path_2', id=2)) self.lib.add(Album(album=u'album', id=3)) self.lib.add(Album(album=u'another album', id=4)) web.app.config['TESTING'] = True web.app.config['lib'] = self.lib + web.app.config['EXCLUDE_PATHS_FROM_ITEMS'] = True self.client = web.app.test_client() + def test_config_exclude_paths_from_items(self): + web.app.config['EXCLUDE_PATHS_FROM_ITEMS'] = False + response = self.client.get('/item/1') + response.json = json.loads(response.data.decode('utf-8')) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json['path'], u'/path_1') + def test_get_all_items(self): response = self.client.get('/item/') response.json = json.loads(response.data.decode('utf-8')) From c409a71ea1d9eeb5785d75d87d77a6652a0360bf Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 10:31:33 -0800 Subject: [PATCH 076/193] Add missing plus signs to web.rst --- docs/plugins/web.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/web.rst b/docs/plugins/web.rst index 93a67e7ea..6bb5b0e4b 100644 --- a/docs/plugins/web.rst +++ b/docs/plugins/web.rst @@ -163,7 +163,7 @@ dropped from the response. ``GET /item/by_path/...`` -+++++++++++++++++++++ ++++++++++++++++++++++++++ Look for an item at the given path on the server. If it corresponds to a track, return the track in the same format as /item/*. From a994df6aa6cce5dff6bee8b08cd8816181b8ffb1 Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 10:34:02 -0800 Subject: [PATCH 077/193] Fix bad doc formatting --- docs/plugins/web.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/web.rst b/docs/plugins/web.rst index 6bb5b0e4b..ef5769a2c 100644 --- a/docs/plugins/web.rst +++ b/docs/plugins/web.rst @@ -166,7 +166,7 @@ dropped from the response. +++++++++++++++++++++++++ Look for an item at the given path on the server. If it corresponds to a track, -return the track in the same format as /item/*. +return the track in the same format as ``/item/*``. ``GET /item/query/querystring`` From 05bc4996a832fa9be7f20e2c5c5f8794001d91e0 Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 11:08:24 -0800 Subject: [PATCH 078/193] Rename and invert new config option --- beetsplug/web/__init__.py | 12 ++++++------ docs/plugins/web.rst | 4 ++-- test/test_web.py | 14 +++++++++++--- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 1344b898d..4d5dcba54 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -37,10 +37,10 @@ def _rep(obj, expand=False): out = dict(obj) if isinstance(obj, beets.library.Item): - if app.config.get('EXCLUDE_PATHS_FROM_ITEMS', True): - del out['path'] - else: + if app.config.get('INCLUDE_PATHS', False): out['path'] = out['path'].decode('utf-8') + else: + del out['path'] # Get the size (in bytes) of the backing file. This is useful # for the Tomahawk resolver API. @@ -320,7 +320,7 @@ class WebPlugin(BeetsPlugin): 'host': u'127.0.0.1', 'port': 8337, 'cors': '', - 'exclude_paths_from_items': True, + 'include_paths': False, }) def commands(self): @@ -339,8 +339,8 @@ class WebPlugin(BeetsPlugin): # Normalizes json output app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False - app.config['EXCLUDE_PATHS_FROM_ITEMS'] = ( - self.config.get('exclude_paths_from_items', True)) + app.config['INCLUDE_PATHS'] = ( + self.config.get('include_paths', False)) # Enable CORS if required. if self.config['cors']: diff --git a/docs/plugins/web.rst b/docs/plugins/web.rst index ef5769a2c..54c507cf0 100644 --- a/docs/plugins/web.rst +++ b/docs/plugins/web.rst @@ -63,8 +63,8 @@ configuration file. The available options are: Default: 8337. - **cors**: The CORS allowed origin (see :ref:`web-cors`, below). Default: CORS is disabled. -- **exclude_paths_from_items**: The 'path' key of items is filtered out of JSON - responses for security reasons. Default: true. +- **include_paths**: If true, includes paths in item objects. + Default: false. Implementation -------------- diff --git a/test/test_web.py b/test/test_web.py index d64341f3f..871fd1b09 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -28,17 +28,25 @@ class WebPluginTest(_common.LibTestCase): web.app.config['TESTING'] = True web.app.config['lib'] = self.lib - web.app.config['EXCLUDE_PATHS_FROM_ITEMS'] = True + web.app.config['INCLUDE_PATHS'] = False self.client = web.app.test_client() - def test_config_exclude_paths_from_items(self): - web.app.config['EXCLUDE_PATHS_FROM_ITEMS'] = False + def test_config_include_paths_true(self): + web.app.config['INCLUDE_PATHS'] = True response = self.client.get('/item/1') response.json = json.loads(response.data.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(response.json['path'], u'/path_1') + def test_config_include_paths_false(self): + web.app.config['INCLUDE_PATHS'] = False + response = self.client.get('/item/1') + response.json = json.loads(response.data.decode('utf-8')) + + self.assertEqual(response.status_code, 200) + self.assertNotIn('path', response.json) + def test_get_all_items(self): response = self.client.get('/item/') response.json = json.loads(response.data.decode('utf-8')) From 866a650bc0d7d3e48484afca2855dd0828e2683d Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 11:15:18 -0800 Subject: [PATCH 079/193] Rename /item/by_path to /item/path and use PathQuery instead of direct file access --- beetsplug/web/__init__.py | 14 ++++++++------ docs/plugins/web.rst | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 4d5dcba54..d47f1d183 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -221,11 +221,14 @@ def item_query(queries): return g.lib.items(queries) -@app.route('/item/at_path/') +@app.route('/item/path/') def item_at_path(path): - try: - return flask.jsonify(_rep(beets.library.Item.from_path('/' + path))) - except beets.library.ReadError: + g.lib._connection().create_function('bytelower', 1, beets.library._sqlite_bytelower) + query = beets.library.PathQuery('path', u'/' + path) + item = g.lib.items(query).get() + if item: + return flask.jsonify(_rep(item)) + else: return flask.abort(404) @@ -339,8 +342,7 @@ class WebPlugin(BeetsPlugin): # Normalizes json output app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False - app.config['INCLUDE_PATHS'] = ( - self.config.get('include_paths', False)) + app.config['INCLUDE_PATHS'] = self.config['include_paths'] # Enable CORS if required. if self.config['cors']: diff --git a/docs/plugins/web.rst b/docs/plugins/web.rst index 54c507cf0..f0adacc00 100644 --- a/docs/plugins/web.rst +++ b/docs/plugins/web.rst @@ -162,8 +162,8 @@ response includes all the items requested. If a track is not found it is silentl dropped from the response. -``GET /item/by_path/...`` -+++++++++++++++++++++++++ +``GET /item/path/...`` +++++++++++++++++++++++ Look for an item at the given path on the server. If it corresponds to a track, return the track in the same format as ``/item/*``. From 4434569ddc9b55c6799e8ef56583185ec9d3b4d6 Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 11:28:42 -0800 Subject: [PATCH 080/193] beets.library.Library adds custom bytelower function to all connections, not just one --- beetsplug/web/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index d47f1d183..360e018e6 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -223,7 +223,6 @@ def item_query(queries): @app.route('/item/path/') def item_at_path(path): - g.lib._connection().create_function('bytelower', 1, beets.library._sqlite_bytelower) query = beets.library.PathQuery('path', u'/' + path) item = g.lib.items(query).get() if item: From d8fbdbc16ad4cd1ce3606042c5400001229207fa Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 11:36:57 -0800 Subject: [PATCH 081/193] Update changelog --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index eaf8c7d5a..be7020dc1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,10 @@ New features: non-numeric track index data. For example, some vinyl or tape media will report the side of the record using a letter instead of a number in that field. :bug:`1831` :bug:`2363` +* The :doc:`/plugins/web` has a new endpoint, ``/item/path/foo``, which will + return the item info for the file at the given path, or 404. +* The :doc:`/plugins/web` also has a new config option, ``include_paths``, + which will cause paths to be included in item API responses if set to true. Fixes: From 6b7a6baaf21ad3d017a0d940b7fcaa10c450a039 Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 12:12:20 -0800 Subject: [PATCH 082/193] Add test for /item/path/ endpoint --- test/test_web.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/test_web.py b/test/test_web.py index 871fd1b09..867e2c3e3 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -4,11 +4,12 @@ from __future__ import division, absolute_import, print_function +import json import unittest +import os.path from six import assertCountEqual from test import _common -import json from beets.library import Item, Album from beetsplug import web @@ -75,6 +76,23 @@ class WebPluginTest(_common.LibTestCase): response = self.client.get('/item/3') self.assertEqual(response.status_code, 404) + def test_get_single_item_by_path(self): + data_path = os.path.join(_common.RSRC, b'full.mp3') + self.lib.add(Item.from_path(data_path)) + response = self.client.get('/item/path' + data_path.decode('utf-8')) + response.json = json.loads(response.data.decode('utf-8')) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json['title'], u'full') + + def test_get_single_item_by_path_not_found_if_not_in_library(self): + data_path = os.path.join(_common.RSRC, b'full.mp3') + # data_path points to a valid file, but we have not added the file + # to the library. + response = self.client.get('/item/path' + data_path.decode('utf-8')) + + self.assertEqual(response.status_code, 404) + def test_get_item_empty_query(self): response = self.client.get('/item/query/') response.json = json.loads(response.data.decode('utf-8')) From e2be6ba7814dafc30f9a025dacf8769687ef26c5 Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 12:44:07 -0800 Subject: [PATCH 083/193] Query path with bytestring. Might fix tests. --- beetsplug/web/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 360e018e6..7977eda94 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -223,7 +223,7 @@ def item_query(queries): @app.route('/item/path/') def item_at_path(path): - query = beets.library.PathQuery('path', u'/' + path) + query = beets.library.PathQuery('path', b'/' + path.encode('utf-8')) item = g.lib.items(query).get() if item: return flask.jsonify(_rep(item)) From e3707e45f3308ab7767dd6e690a388004477b43e Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 20:40:30 -0800 Subject: [PATCH 084/193] Maybe fix code and tests for Windows --- beetsplug/web/__init__.py | 9 +++++++-- test/test_web.py | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 7977eda94..f955b6d63 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -176,11 +176,16 @@ class QueryConverter(PathConverter): return ','.join(value) +class EverythingConverter(PathConverter): + regex = '.*?' + + # Flask setup. app = flask.Flask(__name__) app.url_map.converters['idlist'] = IdListConverter app.url_map.converters['query'] = QueryConverter +app.url_map.converters['everything'] = EverythingConverter @app.before_request @@ -221,9 +226,9 @@ def item_query(queries): return g.lib.items(queries) -@app.route('/item/path/') +@app.route('/item/path/') def item_at_path(path): - query = beets.library.PathQuery('path', b'/' + path.encode('utf-8')) + query = beets.library.PathQuery('path', path.encode('utf-8')) item = g.lib.items(query).get() if item: return flask.jsonify(_rep(item)) diff --git a/test/test_web.py b/test/test_web.py index 867e2c3e3..98347d8af 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -79,7 +79,7 @@ class WebPluginTest(_common.LibTestCase): def test_get_single_item_by_path(self): data_path = os.path.join(_common.RSRC, b'full.mp3') self.lib.add(Item.from_path(data_path)) - response = self.client.get('/item/path' + data_path.decode('utf-8')) + response = self.client.get('/item/path/' + data_path.decode('utf-8')) response.json = json.loads(response.data.decode('utf-8')) self.assertEqual(response.status_code, 200) @@ -89,7 +89,7 @@ class WebPluginTest(_common.LibTestCase): data_path = os.path.join(_common.RSRC, b'full.mp3') # data_path points to a valid file, but we have not added the file # to the library. - response = self.client.get('/item/path' + data_path.decode('utf-8')) + response = self.client.get('/item/path/' + data_path.decode('utf-8')) self.assertEqual(response.status_code, 404) From 1426aea4a28d63d2228692a34678f4bf8071c05d Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sat, 14 Jan 2017 21:06:38 -0800 Subject: [PATCH 085/193] Update docs --- docs/plugins/web.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/plugins/web.rst b/docs/plugins/web.rst index f0adacc00..9f1bbcd99 100644 --- a/docs/plugins/web.rst +++ b/docs/plugins/web.rst @@ -165,8 +165,11 @@ dropped from the response. ``GET /item/path/...`` ++++++++++++++++++++++ -Look for an item at the given path on the server. If it corresponds to a track, -return the track in the same format as ``/item/*``. +Look for an item at the given absolute path on the server. If it corresponds to +a track, return the track in the same format as ``/item/*``. + +If the server runs UNIX, you'll need to include an extra leading slash: +``http://localhost:8337/item/path//Users/beets/Music/Foo/Bar/Baz.mp3`` ``GET /item/query/querystring`` From 926dce241ca759d407dea4e14ca624f827cb0591 Mon Sep 17 00:00:00 2001 From: Steve Johnson Date: Sun, 15 Jan 2017 11:25:03 -0800 Subject: [PATCH 086/193] Use util.displayable_path instead of naive .decode() --- beetsplug/web/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index f955b6d63..bd4677bd8 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -38,7 +38,7 @@ def _rep(obj, expand=False): if isinstance(obj, beets.library.Item): if app.config.get('INCLUDE_PATHS', False): - out['path'] = out['path'].decode('utf-8') + out['path'] = util.displayable_path(out['path']) else: del out['path'] From 958ad430fc4854b2589ec3a2726b7ee269e5322e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 17 Jan 2017 11:37:10 -0800 Subject: [PATCH 087/193] bpd: Use integers for time values (fix 2394) --- beetsplug/bpd/gstplayer.py | 4 ++-- docs/changelog.rst | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/beetsplug/bpd/gstplayer.py b/beetsplug/bpd/gstplayer.py index fffa8a6ed..705692aa5 100644 --- a/beetsplug/bpd/gstplayer.py +++ b/beetsplug/bpd/gstplayer.py @@ -177,12 +177,12 @@ class GstPlayer(object): posq = self.player.query_position(fmt) if not posq[0]: raise QueryError("query_position failed") - pos = posq[1] / (10 ** 9) + pos = posq[1] // (10 ** 9) lengthq = self.player.query_duration(fmt) if not lengthq[0]: raise QueryError("query_duration failed") - length = lengthq[1] / (10 ** 9) + length = lengthq[1] // (10 ** 9) self.cached_time = (pos, length) return (pos, length) diff --git a/docs/changelog.rst b/docs/changelog.rst index be7020dc1..4fa8aa50f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,7 @@ Fixes: * :doc:`/plugins/mpdupdate`: Fix Python 3 compatibility. :bug:`2381` * :doc:`/plugins/replaygain`: Fix Python 3 compatibility in the ``bs1770gain`` backend. :bug:`2382` +* :doc:`/plugins/bpd`: Report playback times as integer. :bug:`2394` 1.4.3 (January 9, 2017) From 5233e0fcddcba20fb9b79dc650dc137197316791 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 19 Jan 2017 11:31:25 -0800 Subject: [PATCH 088/193] Avoid stdout encoding issues (#2393) --- beets/ui/__init__.py | 17 +++++++++++------ docs/changelog.rst | 3 +++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index ae30a9c60..d69870737 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -126,8 +126,7 @@ def print_(*strings, **kwargs): Python 3. The `end` keyword argument behaves similarly to the built-in `print` - (it defaults to a newline). The value should have the same string - type as the arguments. + (it defaults to a newline). """ if not strings: strings = [u''] @@ -136,11 +135,17 @@ def print_(*strings, **kwargs): txt = u' '.join(strings) txt += kwargs.get('end', u'\n') - # Send bytes to the stdout stream on Python 2. + # Encode the string and write it to stdout. + txt = txt.encode(_out_encoding(), 'replace') if six.PY2: - txt = txt.encode(_out_encoding(), 'replace') - - sys.stdout.write(txt) + # On Python 2, sys.stdout expects bytes. + sys.stdout.write(txt) + else: + # On Python 3, sys.stdout expects text strings and uses the + # exception-throwing encoding error policy. To avoid throwing + # errors and use our configurable encoding override, we use the + # underlying bytes buffer instead. + sys.stdout.buffer.write(txt) # Configuration wrappers. diff --git a/docs/changelog.rst b/docs/changelog.rst index 4fa8aa50f..3b920e51a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,9 @@ Fixes: * :doc:`/plugins/replaygain`: Fix Python 3 compatibility in the ``bs1770gain`` backend. :bug:`2382` * :doc:`/plugins/bpd`: Report playback times as integer. :bug:`2394` +* On Python 3, the :ref:`terminal_encoding` setting is respected again for + output and printing will no longer crash on systems configured with a + limited encoding. 1.4.3 (January 9, 2017) From a99d5d2ed2081172a3d486a492319cd87e02d141 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 19 Jan 2017 11:42:46 -0800 Subject: [PATCH 089/193] On Python 3 in tests, record `str` output directly This is a little ugly. Eventually, it would be nice to create a single output stream set up with the appropriate encoding settings *at startup* and use that repeatedly, instead of having `print_` check the settings every time. This output stream could be cleanly replaced with a mock harness for testing. Yet another reason to desire a big "beets context" object... --- beets/ui/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index d69870737..df370b52e 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -136,16 +136,22 @@ def print_(*strings, **kwargs): txt += kwargs.get('end', u'\n') # Encode the string and write it to stdout. - txt = txt.encode(_out_encoding(), 'replace') if six.PY2: # On Python 2, sys.stdout expects bytes. - sys.stdout.write(txt) + out = txt.encode(_out_encoding(), 'replace') + sys.stdout.write(out) else: # On Python 3, sys.stdout expects text strings and uses the # exception-throwing encoding error policy. To avoid throwing # errors and use our configurable encoding override, we use the # underlying bytes buffer instead. - sys.stdout.buffer.write(txt) + if hasattr(sys.stdout, 'buffer'): + out = txt.encode(_out_encoding(), 'replace') + sys.stdout.buffer.write(out) + else: + # In our test harnesses (e.g., DummyOut), sys.stdout.buffer + # does not exist. We instead just record the text string. + sys.stdout.write(txt) # Configuration wrappers. From 377a2a6964caaaff0e8d8a8059d57d65d838a517 Mon Sep 17 00:00:00 2001 From: diomekes Date: Thu, 19 Jan 2017 15:09:12 -0500 Subject: [PATCH 090/193] add bracket argument to aunique --- beets/library.py | 15 ++++++++++++--- docs/reference/pathformat.rst | 25 +++++++++++++++---------- test/test_library.py | 4 ++-- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/beets/library.py b/beets/library.py index e3ac1bd40..1cfe87f0c 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1447,7 +1447,7 @@ class DefaultTemplateFunctions(object): cur_fmt = beets.config['time_format'].as_str() return time.strftime(fmt, time.strptime(s, cur_fmt)) - def tmpl_aunique(self, keys=None, disam=None): + def tmpl_aunique(self, keys=None, disam=None, bracket=None): """Generate a string that is guaranteed to be unique among all albums in the library who share the same set of keys. A fields from "disam" is used in the string if one is sufficient to @@ -1467,8 +1467,10 @@ class DefaultTemplateFunctions(object): keys = keys or 'albumartist album' disam = disam or 'albumtype year label catalognum albumdisambig' + bracket = bracket or '[]' keys = keys.split() disam = disam.split() + bracket = None if bracket is ' ' else list(bracket) album = self.lib.get_album(self.item) if not album: @@ -1502,13 +1504,20 @@ class DefaultTemplateFunctions(object): else: # No disambiguator distinguished all fields. - res = u' {0}'.format(album.id) + res = u' {1}{0}{2}'.format(album.id, bracket[0] if bracket else + u'', bracket[1] if bracket else u'') self.lib._memotable[memokey] = res return res # Flatten disambiguation value into a string. disam_value = album.formatted(True).get(disambiguator) - res = u' [{0}]'.format(disam_value) + res = u' {1}{0}{2}'.format(disam_value, bracket[0] if bracket else u'', + bracket[1] if bracket else u'') + + # Remove space and/or brackets if disambiguation value is empty + if bracket and res == u' ' + ''.join(bracket) or res.isspace(): + res = u'' + self.lib._memotable[memokey] = res return res diff --git a/docs/reference/pathformat.rst b/docs/reference/pathformat.rst index 72453a6e5..de1185ae0 100644 --- a/docs/reference/pathformat.rst +++ b/docs/reference/pathformat.rst @@ -71,8 +71,8 @@ These functions are built in to beets: For example, "café" becomes "cafe". Uses the mapping provided by the `unidecode module`_. See the :ref:`asciify-paths` configuration option. -* ``%aunique{identifiers,disambiguators}``: Provides a unique string to - disambiguate similar albums in the database. See :ref:`aunique`, below. +* ``%aunique{identifiers,disambiguators,brackets}``: Provides a unique string + to disambiguate similar albums in the database. See :ref:`aunique`, below. * ``%time{date_time,format}``: Return the date and time in any format accepted by `strftime`_. For example, to get the year some music was added to your library, use ``%time{$added,%Y}``. @@ -112,14 +112,16 @@ will expand to "[2008]" for one album and "[2010]" for the other. The function detects that you have two albums with the same artist and title but that they have different release years. -For full flexibility, the ``%aunique`` function takes two arguments, each of -which are whitespace-separated lists of album field names: a set of -*identifiers* and a set of *disambiguators*. Any group of albums with identical -values for all the identifiers will be considered "duplicates". Then, the -function tries each disambiguator field, looking for one that distinguishes each -of the duplicate albums from each other. The first such field is used as the -result for ``%aunique``. If no field suffices, an arbitrary number is used to -distinguish the two albums. +For full flexibility, the ``%aunique`` function takes three arguments. The +first two are whitespace-separated lists of album field names: a set of +*identifiers* and a set of *disambiguators*. The third argument is a pair of +characters used to surround the disambiguator. + +Any group of albums with identical values for all the identifiers will be +considered "duplicates". Then, the function tries each disambiguator field, +looking for one that distinguishes each of the duplicate albums from each +other. The first such field is used as the result for ``%aunique``. If no field +suffices, an arbitrary number is used to distinguish the two albums. The default identifiers are ``albumartist album`` and the default disambiguators are ``albumtype year label catalognum albumdisambig``. So you can get reasonable @@ -127,6 +129,9 @@ disambiguation behavior if you just use ``%aunique{}`` with no parameters in your path forms (as in the default path formats), but you can customize the disambiguation if, for example, you include the year by default in path formats. +The default characters used as brackets are ``[]``. If a single blank space is +used, then the disambiguator will not be surrounded by anything. + One caveat: When you import an album that is named identically to one already in your library, the *first* album—the one already in your library— will not consider itself a duplicate at import time. This means that ``%aunique{}`` will diff --git a/test/test_library.py b/test/test_library.py index 7aa88e064..d2c21d5f6 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -713,8 +713,8 @@ class DisambiguationTest(_common.TestCase, PathFormattingMixin): album2.year = 2001 album2.store() - self._assert_dest(b'/base/foo 1/the title', self.i1) - self._assert_dest(b'/base/foo 2/the title', self.i2) + self._assert_dest(b'/base/foo [1]/the title', self.i1) + self._assert_dest(b'/base/foo [2]/the title', self.i2) def test_unique_falls_back_to_second_distinguishing_field(self): self._setf(u'foo%aunique{albumartist album,month year}/$title') From 3a967df3961022f3787a99b19b963a623c3ba3fa Mon Sep 17 00:00:00 2001 From: diomekes Date: Thu, 19 Jan 2017 20:30:13 -0500 Subject: [PATCH 091/193] simplify check for empty disam_val, update changelog and docs, add change bracket test --- beets/library.py | 25 +++++++++++++++++-------- docs/changelog.rst | 5 +++++ docs/reference/pathformat.rst | 6 ++++-- test/test_library.py | 10 ++++++++++ 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/beets/library.py b/beets/library.py index 1cfe87f0c..86fcf7c20 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1453,7 +1453,9 @@ class DefaultTemplateFunctions(object): from "disam" is used in the string if one is sufficient to disambiguate the albums. Otherwise, a fallback opaque value is used. Both "keys" and "disam" should be given as - whitespace-separated lists of field names. + whitespace-separated lists of field names, while "bracket" is a + pair of characters to be used as brackets surrounding the + disambiguator or a white space to have no brackets. """ # Fast paths: no album, no item or library, or memoized value. if not self.item or not self.lib: @@ -1470,7 +1472,15 @@ class DefaultTemplateFunctions(object): bracket = bracket or '[]' keys = keys.split() disam = disam.split() - bracket = None if bracket is ' ' else list(bracket) + bracket = None if bracket == ' ' else list(bracket) + + # Assign a left and right bracket from bracket list. + if bracket: + bracket_l = bracket[0] + bracket_r = bracket[1] + else: + bracket_l = u'' + bracket_r = u'' album = self.lib.get_album(self.item) if not album: @@ -1504,18 +1514,17 @@ class DefaultTemplateFunctions(object): else: # No disambiguator distinguished all fields. - res = u' {1}{0}{2}'.format(album.id, bracket[0] if bracket else - u'', bracket[1] if bracket else u'') + res = u' {1}{0}{2}'.format(album.id, bracket_l, bracket_r) self.lib._memotable[memokey] = res return res # Flatten disambiguation value into a string. disam_value = album.formatted(True).get(disambiguator) - res = u' {1}{0}{2}'.format(disam_value, bracket[0] if bracket else u'', - bracket[1] if bracket else u'') - # Remove space and/or brackets if disambiguation value is empty - if bracket and res == u' ' + ''.join(bracket) or res.isspace(): + # Return empty string if disambiguator is empty. + if disam_value: + res = u' {1}{0}{2}'.format(disam_value, bracket_l, bracket_r) + else: res = u'' self.lib._memotable[memokey] = res diff --git a/docs/changelog.rst b/docs/changelog.rst index 4fa8aa50f..e2849c22d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,11 @@ New features: return the item info for the file at the given path, or 404. * The :doc:`/plugins/web` also has a new config option, ``include_paths``, which will cause paths to be included in item API responses if set to true. +* The ``%aunique`` template function for :ref:`aunique` now takes a third + argument that specifies which brackets to use around the disambiguator + value. The argument can be any two characters that represent the left and + right brackets. It defaults to `[]` and can also be a white space to turn off + bracketing. :bug:`2397` :bug:`2399` Fixes: diff --git a/docs/reference/pathformat.rst b/docs/reference/pathformat.rst index de1185ae0..873a51c5e 100644 --- a/docs/reference/pathformat.rst +++ b/docs/reference/pathformat.rst @@ -129,8 +129,10 @@ disambiguation behavior if you just use ``%aunique{}`` with no parameters in your path forms (as in the default path formats), but you can customize the disambiguation if, for example, you include the year by default in path formats. -The default characters used as brackets are ``[]``. If a single blank space is -used, then the disambiguator will not be surrounded by anything. +The default characters used as brackets are ``[]``. To change this, provide a +third argument to the ``%aunique`` function consisting of two characters: the left +and right brackets. Or, to turn off bracketing entirely, use a single blank +space. One caveat: When you import an album that is named identically to one already in your library, the *first* album—the one already in your library— will not diff --git a/test/test_library.py b/test/test_library.py index d2c21d5f6..9e1301880 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -730,6 +730,16 @@ class DisambiguationTest(_common.TestCase, PathFormattingMixin): self._setf(u'foo%aunique{albumartist album,albumtype}/$title') self._assert_dest(b'/base/foo [foo_bar]/the title', self.i1) + def test_drop_empty_disam_string(self): + album1 = self.lib.get_album(self.i1) + album1.year = None + album1.store() + self._assert_dest(b'/base/foo/the title', self.i1) + + def test_change_brackets(self): + self._setf(u'foo%aunique{albumartist album,year,()}/$title') + self._assert_dest(b'/base/foo (2001)/the title', self.i1) + class PluginDestinationTest(_common.TestCase): def setUp(self): From d10df34c65bbdcff39c9c7660ae4b9f5593cd26c Mon Sep 17 00:00:00 2001 From: diomekes Date: Fri, 20 Jan 2017 09:06:38 -0500 Subject: [PATCH 092/193] add test for aunique without brackets --- test/test_library.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_library.py b/test/test_library.py index 9e1301880..263254f6b 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -740,6 +740,10 @@ class DisambiguationTest(_common.TestCase, PathFormattingMixin): self._setf(u'foo%aunique{albumartist album,year,()}/$title') self._assert_dest(b'/base/foo (2001)/the title', self.i1) + def test_remove_brackets(self): + self._setf(u'foo%aunique{albumartist album,year, }/$title') + self._assert_dest(b'/base/foo 2001/the title', self.i1) + class PluginDestinationTest(_common.TestCase): def setUp(self): From eaa2161a903ba8ec5d33f3497100e1900fa468a5 Mon Sep 17 00:00:00 2001 From: diomekes Date: Fri, 20 Jan 2017 19:40:09 -0500 Subject: [PATCH 093/193] fix empty disambig string test --- test/test_library.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/test_library.py b/test/test_library.py index 263254f6b..34e5437e5 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -730,10 +730,14 @@ class DisambiguationTest(_common.TestCase, PathFormattingMixin): self._setf(u'foo%aunique{albumartist album,albumtype}/$title') self._assert_dest(b'/base/foo [foo_bar]/the title', self.i1) - def test_drop_empty_disam_string(self): + def test_drop_empty_disambig_string(self): album1 = self.lib.get_album(self.i1) - album1.year = None + album1.albumdisambig = None + album2 = self.lib.get_album(self.i2) + album2.albumdisambig = u'foo' album1.store() + album2.store() + self._setf(u'foo%aunique{albumartist album,albumdisambig}/$title') self._assert_dest(b'/base/foo/the title', self.i1) def test_change_brackets(self): From 04f7915d41c246c7d8305d079fd1b130389b5a61 Mon Sep 17 00:00:00 2001 From: diomekes Date: Fri, 20 Jan 2017 22:47:47 -0500 Subject: [PATCH 094/193] change no-bracket argument from white space to empty --- beets/library.py | 9 +++++---- docs/changelog.rst | 2 +- docs/reference/pathformat.rst | 3 +-- test/test_library.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/beets/library.py b/beets/library.py index 86fcf7c20..6f68e7434 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1469,13 +1469,14 @@ class DefaultTemplateFunctions(object): keys = keys or 'albumartist album' disam = disam or 'albumtype year label catalognum albumdisambig' - bracket = bracket or '[]' + if bracket is None: + bracket = '[]' keys = keys.split() disam = disam.split() - bracket = None if bracket == ' ' else list(bracket) - # Assign a left and right bracket from bracket list. - if bracket: + # Assign a left and right bracket or leave blank if argument is empty. + if len(bracket) == 2: + bracket = list(bracket) bracket_l = bracket[0] bracket_r = bracket[1] else: diff --git a/docs/changelog.rst b/docs/changelog.rst index e2849c22d..0a32752d1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,7 +20,7 @@ New features: * The ``%aunique`` template function for :ref:`aunique` now takes a third argument that specifies which brackets to use around the disambiguator value. The argument can be any two characters that represent the left and - right brackets. It defaults to `[]` and can also be a white space to turn off + right brackets. It defaults to `[]` and can also be blank to turn off bracketing. :bug:`2397` :bug:`2399` Fixes: diff --git a/docs/reference/pathformat.rst b/docs/reference/pathformat.rst index 873a51c5e..667be3150 100644 --- a/docs/reference/pathformat.rst +++ b/docs/reference/pathformat.rst @@ -131,8 +131,7 @@ disambiguation if, for example, you include the year by default in path formats. The default characters used as brackets are ``[]``. To change this, provide a third argument to the ``%aunique`` function consisting of two characters: the left -and right brackets. Or, to turn off bracketing entirely, use a single blank -space. +and right brackets. Or, to turn off bracketing entirely, leave argument blank. One caveat: When you import an album that is named identically to one already in your library, the *first* album—the one already in your library— will not diff --git a/test/test_library.py b/test/test_library.py index 34e5437e5..aaab6fe03 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -745,7 +745,7 @@ class DisambiguationTest(_common.TestCase, PathFormattingMixin): self._assert_dest(b'/base/foo (2001)/the title', self.i1) def test_remove_brackets(self): - self._setf(u'foo%aunique{albumartist album,year, }/$title') + self._setf(u'foo%aunique{albumartist album,year,}/$title') self._assert_dest(b'/base/foo 2001/the title', self.i1) From 672fc36cc8b3c51d7332a4dbba2a9613e36d17d4 Mon Sep 17 00:00:00 2001 From: diomekes Date: Fri, 20 Jan 2017 23:08:36 -0500 Subject: [PATCH 095/193] fix docstring for aunique empty bracket --- beets/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/library.py b/beets/library.py index 6f68e7434..a63694804 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1455,7 +1455,7 @@ class DefaultTemplateFunctions(object): used. Both "keys" and "disam" should be given as whitespace-separated lists of field names, while "bracket" is a pair of characters to be used as brackets surrounding the - disambiguator or a white space to have no brackets. + disambiguator or empty to have no brackets. """ # Fast paths: no album, no item or library, or memoized value. if not self.item or not self.lib: From c2b6623aaccd82aa695ba9583e34b879fa674b1a Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 20 Jan 2017 20:47:03 -0800 Subject: [PATCH 096/193] Remove an unnecessary list conversion (#2399) Indexing a string works just fine. --- beets/library.py | 1 - 1 file changed, 1 deletion(-) diff --git a/beets/library.py b/beets/library.py index a63694804..4e5d9ccf6 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1476,7 +1476,6 @@ class DefaultTemplateFunctions(object): # Assign a left and right bracket or leave blank if argument is empty. if len(bracket) == 2: - bracket = list(bracket) bracket_l = bracket[0] bracket_r = bracket[1] else: From c21f456f032864030adde9916b9a0aae9edb9851 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 20 Jan 2017 20:47:40 -0800 Subject: [PATCH 097/193] tox: Use Python 3.6 in default tests Just typing `tox` should use the most relevant Python versions. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8115395c6..43bff8014 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27-test, py35-test, py27-flake8, docs +envlist = py27-test, py36-test, py27-flake8, docs # The exhaustive list of environments is: # envlist = py{27,34,35}-{test,cov}, py{27,34,35}-flake8, docs From ca904a9d0c93cee65eedad3a0d67b6ddbc7fd5fb Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 21 Jan 2017 23:21:11 -0800 Subject: [PATCH 098/193] mpdstats: Fix Python 3 compatibility (fix #2405) We previously needed a hack to get the client to consume and produce Unicode strings. The library has since added Unicode support, behind a constructor flag. We can remove the hack now, which was causing a crash on Python 3 because the library uses Unicode by default there, and instead use its built-in support. --- beetsplug/mpdstats.py | 16 +--------------- docs/changelog.rst | 2 ++ setup.py | 2 +- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 89b99a9df..4258cbc4c 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -46,20 +46,6 @@ def is_url(path): return path.split('://', 1)[0] in ['http', 'https'] -# Use the MPDClient internals to get unicode. -# see http://www.tarmack.eu/code/mpdunicode.py for the general idea -class MPDClient(mpd.MPDClient): - def _write_command(self, command, args=[]): - args = [six.text_type(arg).encode('utf-8') for arg in args] - super(MPDClient, self)._write_command(command, args) - - def _read_line(self): - line = super(MPDClient, self)._read_line() - if line is not None: - return line.decode('utf-8') - return None - - class MPDClientWrapper(object): def __init__(self, log): self._log = log @@ -67,7 +53,7 @@ class MPDClientWrapper(object): self.music_directory = ( mpd_config['music_directory'].as_str()) - self.client = MPDClient() + self.client = mpd.MPDClient(use_unicode=True) def connect(self): """Connect to the MPD. diff --git a/docs/changelog.rst b/docs/changelog.rst index 0a32752d1..667d9b99f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,6 +29,8 @@ Fixes: * :doc:`/plugins/replaygain`: Fix Python 3 compatibility in the ``bs1770gain`` backend. :bug:`2382` * :doc:`/plugins/bpd`: Report playback times as integer. :bug:`2394` +* :doc:`/plugins/mpdstats`: Fix Python 3 compatibility. The plugin also now + requires version 0.4.2 or later of the ``python-mpd2`` library. :bug:`2405` 1.4.3 (January 9, 2017) diff --git a/setup.py b/setup.py index 35131cb55..1019a74a7 100755 --- a/setup.py +++ b/setup.py @@ -117,7 +117,7 @@ setup( 'discogs': ['discogs-client>=2.1.0'], 'beatport': ['requests-oauthlib>=0.6.1'], 'lastgenre': ['pylast'], - 'mpdstats': ['python-mpd2'], + 'mpdstats': ['python-mpd2>=0.4.2'], 'web': ['flask', 'flask-cors'], 'import': ['rarfile'], 'thumbnails': ['pyxdg'] + From 44963598e8bdfed1fcf064b947aad1012123acc5 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 21 Jan 2017 23:32:36 -0800 Subject: [PATCH 099/193] Remove unused import --- beetsplug/mpdstats.py | 1 - 1 file changed, 1 deletion(-) diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 4258cbc4c..896a34415 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -27,7 +27,6 @@ from beets import plugins from beets import library from beets.util import displayable_path from beets.dbcore import types -import six # If we lose the connection, how many times do we want to retry and how # much time should we wait between retries? From 3a6967eb7a4909117466fd20bbc577bf807be70e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 22 Jan 2017 12:59:04 -0800 Subject: [PATCH 100/193] Remove dependencies from convert_stub.py See: https://github.com/beetbox/beets/pull/2403#issuecomment-274358494 --- test/rsrc/convert_stub.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/rsrc/convert_stub.py b/test/rsrc/convert_stub.py index 3ca71352a..f32bce09a 100755 --- a/test/rsrc/convert_stub.py +++ b/test/rsrc/convert_stub.py @@ -6,15 +6,19 @@ a specified text tag. """ from __future__ import division, absolute_import, print_function -from os.path import dirname, abspath -import six import sys import platform +import locale -beets_src = dirname(dirname(dirname(abspath(__file__)))) -sys.path.insert(0, beets_src) +PY2 = sys.version_info[0] == 2 -from beets.util import arg_encoding # noqa: E402 + +# From `beets.util`. +def arg_encoding(): + try: + return locale.getdefaultlocale()[1] or 'utf-8' + except ValueError: + return 'utf-8' def convert(in_file, out_file, tag): @@ -27,7 +31,7 @@ def convert(in_file, out_file, tag): # On Windows, use Unicode paths. (The test harness gives them to us # as UTF-8 bytes.) if platform.system() == 'Windows': - if not six.PY2: + if not PY2: in_file = in_file.encode(arg_encoding()) out_file = out_file.encode(arg_encoding()) in_file = in_file.decode('utf-8') From 77d155cdea473873b8e179eefa180901f12c1623 Mon Sep 17 00:00:00 2001 From: Pauligrinder Date: Mon, 23 Jan 2017 12:43:40 +0200 Subject: [PATCH 101/193] Add a plugin to update a Kodi music library I created one for an older version before, but it didn't work since the change to Python 3. So I created a new one that works. --- beetsplug/kodiupdate.py | 66 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 beetsplug/kodiupdate.py diff --git a/beetsplug/kodiupdate.py b/beetsplug/kodiupdate.py new file mode 100644 index 000000000..236f914bf --- /dev/null +++ b/beetsplug/kodiupdate.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +"""Updates a Kodi library whenever the beets library is changed. This is based on the Plex Update plugin. + +Put something like the following in your config.yaml to configure: + kodi: + host: localhost + port: 8080 + user: user + pwd: secret +""" +from __future__ import division, absolute_import, print_function + +import requests +import json +from requests.auth import HTTPBasicAuth +from beets import config +from beets.plugins import BeetsPlugin + + +def update_kodi(host, port, user, password): + """Sends request to the Kodi api to start a library refresh. + """ + url = "http://{0}:{1}/jsonrpc/".format(host, port) + + # The kodi jsonrpc documentation states that Content-Type: application/json is mandatory + headers = {'Content-Type': 'application/json'} + # Create the payload. Id seems to be mandatory. + payload = {'jsonrpc': '2.0', 'method':'AudioLibrary.Scan', 'id':1} + r = requests.post(url, auth=HTTPBasicAuth(user, password), json=payload, headers=headers) + return r + +class KodiUpdate(BeetsPlugin): + def __init__(self): + super(KodiUpdate, self).__init__() + + # Adding defaults. + config['kodi'].add({ + u'host': u'localhost', + u'port': 8080, + u'user': u'kodi', + u'pwd': u'kodi'}) + + self.register_listener('database_change', self.listen_for_db_change) + + def listen_for_db_change(self, lib, model): + """Listens for beets db change and register the update for the end""" + self.register_listener('cli_exit', self.update) + + def update(self, lib): + """When the client exists try to send refresh request to Kodi server. + """ + self._log.info(u'Updating Kodi library...') + + # Try to send update request. + try: + update_kodi( + config['kodi']['host'].get(), + config['kodi']['port'].get(), + config['kodi']['user'].get(), + config['kodi']['pwd'].get()) + self._log.info(u'... started.') + + except requests.exceptions.RequestException: + self._log.warning(u'Update failed.') + From ca8832809daf6e57b5bd61a3b2c03640b22dbe9c Mon Sep 17 00:00:00 2001 From: Pauligrinder Date: Mon, 23 Jan 2017 13:14:36 +0200 Subject: [PATCH 102/193] Removed a couple of unnecessary imports json and requests.BasicAuthentication --- beetsplug/kodiupdate.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/beetsplug/kodiupdate.py b/beetsplug/kodiupdate.py index 236f914bf..31c978dd6 100644 --- a/beetsplug/kodiupdate.py +++ b/beetsplug/kodiupdate.py @@ -12,8 +12,6 @@ Put something like the following in your config.yaml to configure: from __future__ import division, absolute_import, print_function import requests -import json -from requests.auth import HTTPBasicAuth from beets import config from beets.plugins import BeetsPlugin @@ -27,7 +25,7 @@ def update_kodi(host, port, user, password): headers = {'Content-Type': 'application/json'} # Create the payload. Id seems to be mandatory. payload = {'jsonrpc': '2.0', 'method':'AudioLibrary.Scan', 'id':1} - r = requests.post(url, auth=HTTPBasicAuth(user, password), json=payload, headers=headers) + r = requests.post(url, auth=(user, password), json=payload, headers=headers) return r class KodiUpdate(BeetsPlugin): From 2ab1f3ae89eb74c43acbcf8addfcdb5a52b31106 Mon Sep 17 00:00:00 2001 From: Artem Utin Date: Mon, 23 Jan 2017 23:15:14 +1000 Subject: [PATCH 103/193] More general approach to multiple on_play calls for the same song - now it ignores such calls, if time between calls was below threshold --- beetsplug/mpdstats.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 896a34415..9f63181b9 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -263,25 +263,27 @@ class MPDStats(object): played, duration = map(int, status['time'].split(':', 1)) remaining = duration - played - if self.now_playing and self.now_playing['path'] != path: - skipped = self.handle_song_change(self.now_playing) - # mpd responds twice on a natural new song start - going_to_happen_twice = not skipped - else: - going_to_happen_twice = False + if self.now_playing: + if self.now_playing['path'] != path: + self.handle_song_change(self.now_playing) + else: # in case we got mpd play event with same song playing multiple times + # assume low diff means redundant second play event after natural song start + diff = abs(time.time() - self.now_playing['started']) - if not going_to_happen_twice: - self._log.info(u'playing {0}', displayable_path(path)) + if diff <= self.time_threshold: + return - self.now_playing = { - 'started': time.time(), - 'remaining': remaining, - 'path': path, - 'beets_item': self.get_item(path), - } + self._log.info(u'playing {0}', displayable_path(path)) - self.update_item(self.now_playing['beets_item'], - 'last_played', value=int(time.time())) + self.now_playing = { + 'started': time.time(), + 'remaining': remaining, + 'path': path, + 'beets_item': self.get_item(path), + } + + self.update_item(self.now_playing['beets_item'], + 'last_played', value=int(time.time())) def run(self): self.mpd.connect() From 40eef2056cedee5b9cc5f1d01d89a67ca85fc8ee Mon Sep 17 00:00:00 2001 From: Artem Utin Date: Tue, 24 Jan 2017 11:41:26 +1000 Subject: [PATCH 104/193] Add changelog entry, wrap long comment lines. --- beetsplug/mpdstats.py | 7 +++++-- docs/changelog.rst | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 9f63181b9..bcc3da808 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -266,8 +266,11 @@ class MPDStats(object): if self.now_playing: if self.now_playing['path'] != path: self.handle_song_change(self.now_playing) - else: # in case we got mpd play event with same song playing multiple times - # assume low diff means redundant second play event after natural song start + else: + # In case we got mpd play event with same song playing + # multiple times, + # assume low diff means redundant second play event + # after natural song start. diff = abs(time.time() - self.now_playing['started']) if diff <= self.time_threshold: diff --git a/docs/changelog.rst b/docs/changelog.rst index 667d9b99f..3ab7940e2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,6 +31,7 @@ Fixes: * :doc:`/plugins/bpd`: Report playback times as integer. :bug:`2394` * :doc:`/plugins/mpdstats`: Fix Python 3 compatibility. The plugin also now requires version 0.4.2 or later of the ``python-mpd2`` library. :bug:`2405` +* :doc:`/plugins/mpdstats`: Improve handling of mpd status queries. 1.4.3 (January 9, 2017) From bc582701ffca952e2e618912518b8e5c994fa5dc Mon Sep 17 00:00:00 2001 From: wordofglass Date: Wed, 25 Jan 2017 00:01:05 +0100 Subject: [PATCH 105/193] fetchart: Internally pass settings in a cleaner way. Dump the 'extra' dictionary which only duplicated attributes of the plugin onject in favor of passing a reference to the plugin directly. --- beetsplug/fetchart.py | 88 ++++++++++++++++++------------------------- test/test_art.py | 69 ++++++++++++++++++--------------- 2 files changed, 76 insertions(+), 81 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index d87a5dc48..91b77c0a4 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -69,7 +69,7 @@ class Candidate(object): self.match = match self.size = size - def _validate(self, extra): + def _validate(self, plugin): """Determine whether the candidate artwork is valid based on its dimensions (width and ratio). @@ -80,9 +80,7 @@ class Candidate(object): if not self.path: return self.CANDIDATE_BAD - if not (extra['enforce_ratio'] or - extra['minwidth'] or - extra['maxwidth']): + if not (plugin.enforce_ratio or plugin.minwidth or plugin.maxwidth): return self.CANDIDATE_EXACT # get_size returns None if no local imaging backend is available @@ -101,22 +99,22 @@ class Candidate(object): long_edge = max(self.size) # Check minimum size. - if extra['minwidth'] and self.size[0] < extra['minwidth']: + if plugin.minwidth and self.size[0] < plugin.minwidth: self._log.debug(u'image too small ({} < {})', - self.size[0], extra['minwidth']) + self.size[0], plugin.minwidth) return self.CANDIDATE_BAD # Check aspect ratio. edge_diff = long_edge - short_edge - if extra['enforce_ratio']: - if extra['margin_px']: - if edge_diff > extra['margin_px']: + if plugin.enforce_ratio: + if plugin.margin_px: + if edge_diff > plugin.margin_px: self._log.debug(u'image is not close enough to being ' u'square, ({} - {} > {})', - long_edge, short_edge, extra['margin_px']) + long_edge, short_edge, plugin.margin_px) return self.CANDIDATE_BAD - elif extra['margin_percent']: - margin_px = extra['margin_percent'] * long_edge + elif plugin.margin_percent: + margin_px = plugin.margin_percent * long_edge if edge_diff > margin_px: self._log.debug(u'image is not close enough to being ' u'square, ({} - {} > {})', @@ -129,20 +127,20 @@ class Candidate(object): return self.CANDIDATE_BAD # Check maximum size. - if extra['maxwidth'] and self.size[0] > extra['maxwidth']: + if plugin.maxwidth and self.size[0] > plugin.maxwidth: self._log.debug(u'image needs resizing ({} > {})', - self.size[0], extra['maxwidth']) + self.size[0], plugin.maxwidth) return self.CANDIDATE_DOWNSCALE return self.CANDIDATE_EXACT - def validate(self, extra): - self.check = self._validate(extra) + def validate(self, plugin): + self.check = self._validate(plugin) return self.check - def resize(self, extra): - if extra['maxwidth'] and self.check == self.CANDIDATE_DOWNSCALE: - self.path = ArtResizer.shared.resize(extra['maxwidth'], self.path) + def resize(self, plugin): + if plugin.maxwidth and self.check == self.CANDIDATE_DOWNSCALE: + self.path = ArtResizer.shared.resize(plugin.maxwidth, self.path) def _logged_get(log, *args, **kwargs): @@ -198,13 +196,13 @@ class ArtSource(RequestMixin): self._log = log self._config = config - def get(self, album, extra): + def get(self, album, plugin, paths): raise NotImplementedError() def _candidate(self, **kwargs): return Candidate(source=self, log=self._log, **kwargs) - def fetch_image(self, candidate, extra): + def fetch_image(self, candidate, plugin): raise NotImplementedError() @@ -212,7 +210,7 @@ class LocalArtSource(ArtSource): IS_LOCAL = True LOC_STR = u'local' - def fetch_image(self, candidate, extra): + def fetch_image(self, candidate, plugin): pass @@ -220,13 +218,13 @@ class RemoteArtSource(ArtSource): IS_LOCAL = False LOC_STR = u'remote' - def fetch_image(self, candidate, extra): + def fetch_image(self, candidate, plugin): """Downloads an image from a URL and checks whether it seems to actually be an image. If so, returns a path to the downloaded image. Otherwise, returns None. """ - if extra['maxwidth']: - candidate.url = ArtResizer.shared.proxy_url(extra['maxwidth'], + if plugin.maxwidth: + candidate.url = ArtResizer.shared.proxy_url(plugin.maxwidth, candidate.url) try: with closing(self.request(candidate.url, stream=True, @@ -299,7 +297,7 @@ class CoverArtArchive(RemoteArtSource): URL = 'http://coverartarchive.org/release/{mbid}/front' GROUP_URL = 'http://coverartarchive.org/release-group/{mbid}/front' - def get(self, album, extra): + def get(self, album, plugin, paths): """Return the Cover Art Archive and Cover Art Archive release group URLs using album MusicBrainz release ID and release group ID. """ @@ -317,7 +315,7 @@ class Amazon(RemoteArtSource): URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' INDICES = (1, 2) - def get(self, album, extra): + def get(self, album, plugin, paths): """Generate URLs using Amazon ID (ASIN) string. """ if album.asin: @@ -331,7 +329,7 @@ class AlbumArtOrg(RemoteArtSource): URL = 'http://www.albumart.org/index_detail.php' PAT = r'href\s*=\s*"([^>"]*)"[^>]*title\s*=\s*"View larger image"' - def get(self, album, extra): + def get(self, album, plugin, paths): """Return art URL from AlbumArt.org using album ASIN. """ if not album.asin: @@ -362,7 +360,7 @@ class GoogleImages(RemoteArtSource): self.key = self._config['google_key'].get(), self.cx = self._config['google_engine'].get(), - def get(self, album, extra): + def get(self, album, plugin, paths): """Return art URL from google custom search engine given an album title and interpreter. """ @@ -406,7 +404,7 @@ class FanartTV(RemoteArtSource): super(FanartTV, self).__init__(*args, **kwargs) self.client_key = self._config['fanarttv_key'].get() - def get(self, album, extra): + def get(self, album, plugin, paths): if not album.mb_releasegroupid: return @@ -457,7 +455,7 @@ class FanartTV(RemoteArtSource): class ITunesStore(RemoteArtSource): NAME = u"iTunes Store" - def get(self, album, extra): + def get(self, album, plugin, paths): """Return art URL from iTunes Store given an album title. """ if not (album.albumartist and album.album): @@ -515,7 +513,7 @@ class Wikipedia(RemoteArtSource): }} Limit 1''' - def get(self, album, extra): + def get(self, album, plugin, paths): if not (album.albumartist and album.album): return @@ -627,16 +625,14 @@ class FileSystem(LocalArtSource): """ return [idx for (idx, x) in enumerate(cover_names) if x in filename] - def get(self, album, extra): + def get(self, album, plugin, paths): """Look for album art files in the specified directories. """ - paths = extra['paths'] if not paths: return - cover_names = list(map(util.bytestring_path, extra['cover_names'])) + cover_names = list(map(util.bytestring_path, plugin.cover_names)) cover_names_str = b'|'.join(cover_names) cover_pat = br''.join([br"(\b|_)(", cover_names_str, br")(\b|_)"]) - cautious = extra['cautious'] for path in paths: if not os.path.isdir(syspath(path)): @@ -666,7 +662,7 @@ class FileSystem(LocalArtSource): remaining.append(fn) # Fall back to any image in the folder. - if remaining and not cautious: + if remaining and not plugin.cautious: self._log.debug(u'using fallback art file {0}', util.displayable_path(remaining[0])) yield self._candidate(path=os.path.join(path, remaining[0]), @@ -845,16 +841,6 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): """ out = None - # all the information any of the sources might need - extra = {'paths': paths, - 'cover_names': self.cover_names, - 'cautious': self.cautious, - 'enforce_ratio': self.enforce_ratio, - 'margin_px': self.margin_px, - 'margin_percent': self.margin_percent, - 'minwidth': self.minwidth, - 'maxwidth': self.maxwidth} - for source in self.sources: if source.IS_LOCAL or not local_only: self._log.debug( @@ -864,9 +850,9 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): ) # URLs might be invalid at this point, or the image may not # fulfill the requirements - for candidate in source.get(album, extra): - source.fetch_image(candidate, extra) - if candidate.validate(extra): + for candidate in source.get(album, self, paths): + source.fetch_image(candidate, self) + if candidate.validate(self): out = candidate self._log.debug( u'using {0.LOC_STR} image {1}'.format( @@ -876,7 +862,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): break if out: - out.resize(extra) + out.resize(self) return out diff --git a/test/test_art.py b/test/test_art.py index 50ff26b00..d47b280c2 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -39,6 +39,13 @@ from beets.util import confit logger = logging.getLogger('beets.test_art') +class Settings(): + """Used to pass settings to the ArtSources when the plugin isn't fully + instantiated. + """ + pass + + class UseThePlugin(_common.TestCase): def setUp(self): super(UseThePlugin, self).setUp() @@ -73,28 +80,29 @@ class FetchImageTest(FetchImageHelper, UseThePlugin): super(FetchImageTest, self).setUp() self.dpath = os.path.join(self.temp_dir, b'arttest') self.source = fetchart.RemoteArtSource(logger, self.plugin.config) - self.extra = {'maxwidth': 0} + self.settings = Settings() + self.settings.maxwidth = 0 self.candidate = fetchart.Candidate(logger, url=self.URL) def test_invalid_type_returns_none(self): self.mock_response(self.URL, 'image/watercolour') - self.source.fetch_image(self.candidate, self.extra) + self.source.fetch_image(self.candidate, self.settings) self.assertEqual(self.candidate.path, None) def test_jpeg_type_returns_path(self): self.mock_response(self.URL, 'image/jpeg') - self.source.fetch_image(self.candidate, self.extra) + self.source.fetch_image(self.candidate, self.settings) self.assertNotEqual(self.candidate.path, None) def test_extension_set_by_content_type(self): self.mock_response(self.URL, 'image/png') - self.source.fetch_image(self.candidate, self.extra) + self.source.fetch_image(self.candidate, self.settings) self.assertEqual(os.path.splitext(self.candidate.path)[1], b'.png') self.assertExists(self.candidate.path) def test_does_not_rely_on_server_content_type(self): self.mock_response(self.URL, 'image/jpeg', 'image/png') - self.source.fetch_image(self.candidate, self.extra) + self.source.fetch_image(self.candidate, self.settings) self.assertEqual(os.path.splitext(self.candidate.path)[1], b'.png') self.assertExists(self.candidate.path) @@ -106,44 +114,44 @@ class FSArtTest(UseThePlugin): os.mkdir(self.dpath) self.source = fetchart.FileSystem(logger, self.plugin.config) - self.extra = {'cautious': False, - 'cover_names': ('art',), - 'paths': [self.dpath]} + self.settings = Settings() + self.settings.cautious = False + self.settings.cover_names = ('art',) def test_finds_jpg_in_directory(self): _common.touch(os.path.join(self.dpath, b'a.jpg')) - candidate = next(self.source.get(None, self.extra)) + candidate = next(self.source.get(None, self.settings, [self.dpath])) self.assertEqual(candidate.path, os.path.join(self.dpath, b'a.jpg')) def test_appropriately_named_file_takes_precedence(self): _common.touch(os.path.join(self.dpath, b'a.jpg')) _common.touch(os.path.join(self.dpath, b'art.jpg')) - candidate = next(self.source.get(None, self.extra)) + candidate = next(self.source.get(None, self.settings, [self.dpath])) self.assertEqual(candidate.path, os.path.join(self.dpath, b'art.jpg')) def test_non_image_file_not_identified(self): _common.touch(os.path.join(self.dpath, b'a.txt')) with self.assertRaises(StopIteration): - next(self.source.get(None, self.extra)) + next(self.source.get(None, self.settings, [self.dpath])) def test_cautious_skips_fallback(self): _common.touch(os.path.join(self.dpath, b'a.jpg')) - self.extra['cautious'] = True + self.settings.cautious = True with self.assertRaises(StopIteration): - next(self.source.get(None, self.extra)) + next(self.source.get(None, self.settings, [self.dpath])) def test_empty_dir(self): with self.assertRaises(StopIteration): - next(self.source.get(None, self.extra)) + next(self.source.get(None, self.settings, [self.dpath])) def test_precedence_amongst_correct_files(self): images = [b'front-cover.jpg', b'front.jpg', b'back.jpg'] paths = [os.path.join(self.dpath, i) for i in images] for p in paths: _common.touch(p) - self.extra['cover_names'] = ['cover', 'front', 'back'] + self.settings.cover_names = ['cover', 'front', 'back'] candidates = [candidate.path for candidate in - self.source.get(None, self.extra)] + self.source.get(None, self.settings, [self.dpath])] self.assertEqual(candidates, paths) @@ -236,7 +244,7 @@ class AAOTest(UseThePlugin): def setUp(self): super(AAOTest, self).setUp() self.source = fetchart.AlbumArtOrg(logger, self.plugin.config) - self.extra = dict() + self.settings = Settings() @responses.activate def run(self, *args, **kwargs): @@ -256,21 +264,21 @@ class AAOTest(UseThePlugin): """ self.mock_response(self.AAO_URL, body) album = _common.Bag(asin=self.ASIN) - candidate = next(self.source.get(album, self.extra)) + candidate = next(self.source.get(album, self.settings, [])) self.assertEqual(candidate.url, 'TARGET_URL') def test_aao_scraper_returns_no_result_when_no_image_present(self): self.mock_response(self.AAO_URL, 'blah blah') album = _common.Bag(asin=self.ASIN) with self.assertRaises(StopIteration): - next(self.source.get(album, self.extra)) + next(self.source.get(album, self.settings, [])) class GoogleImageTest(UseThePlugin): def setUp(self): super(GoogleImageTest, self).setUp() self.source = fetchart.GoogleImages(logger, self.plugin.config) - self.extra = dict() + self.settings = Settings() @responses.activate def run(self, *args, **kwargs): @@ -284,7 +292,7 @@ class GoogleImageTest(UseThePlugin): album = _common.Bag(albumartist="some artist", album="some album") json = '{"items": [{"link": "url_to_the_image"}]}' self.mock_response(fetchart.GoogleImages.URL, json) - candidate = next(self.source.get(album, self.extra)) + candidate = next(self.source.get(album, self.settings, [])) self.assertEqual(candidate.url, 'url_to_the_image') def test_google_art_returns_no_result_when_error_received(self): @@ -292,14 +300,14 @@ class GoogleImageTest(UseThePlugin): json = '{"error": {"errors": [{"reason": "some reason"}]}}' self.mock_response(fetchart.GoogleImages.URL, json) with self.assertRaises(StopIteration): - next(self.source.get(album, self.extra)) + next(self.source.get(album, self.settings, [])) def test_google_art_returns_no_result_with_malformed_response(self): album = _common.Bag(albumartist="some artist", album="some album") json = """bla blup""" self.mock_response(fetchart.GoogleImages.URL, json) with self.assertRaises(StopIteration): - next(self.source.get(album, self.extra)) + next(self.source.get(album, self.settings, [])) class FanartTVTest(UseThePlugin): @@ -363,7 +371,7 @@ class FanartTVTest(UseThePlugin): def setUp(self): super(FanartTVTest, self).setUp() self.source = fetchart.FanartTV(logger, self.plugin.config) - self.extra = dict() + self.settings = Settings() @responses.activate def run(self, *args, **kwargs): @@ -377,7 +385,7 @@ class FanartTVTest(UseThePlugin): album = _common.Bag(mb_releasegroupid=u'thereleasegroupid') self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', self.RESPONSE_MULTIPLE) - candidate = next(self.source.get(album, self.extra)) + candidate = next(self.source.get(album, self.settings, [])) self.assertEqual(candidate.url, 'http://example.com/1.jpg') def test_fanarttv_returns_no_result_when_error_received(self): @@ -385,14 +393,14 @@ class FanartTVTest(UseThePlugin): self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', self.RESPONSE_ERROR) with self.assertRaises(StopIteration): - next(self.source.get(album, self.extra)) + next(self.source.get(album, self.settings, [])) def test_fanarttv_returns_no_result_with_malformed_response(self): album = _common.Bag(mb_releasegroupid=u'thereleasegroupid') self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', self.RESPONSE_MALFORMED) with self.assertRaises(StopIteration): - next(self.source.get(album, self.extra)) + next(self.source.get(album, self.settings, [])) def test_fanarttv_only_other_images(self): # The source used to fail when there were images present, but no cover @@ -400,7 +408,7 @@ class FanartTVTest(UseThePlugin): self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', self.RESPONSE_NO_ART) with self.assertRaises(StopIteration): - next(self.source.get(album, self.extra)) + next(self.source.get(album, self.settings, [])) @_common.slow_test() @@ -528,8 +536,8 @@ class ArtForAlbumTest(UseThePlugin): self.old_fs_source_get = fetchart.FileSystem.get - def fs_source_get(_self, album, extra): - if extra['paths']: + def fs_source_get(_self, album, settings, paths): + if paths: yield fetchart.Candidate(logger, path=self.image_file) fetchart.FileSystem.get = fs_source_get @@ -657,5 +665,6 @@ class EnforceRatioConfigTest(_common.TestCase): def suite(): return unittest.TestLoader().loadTestsFromName(__name__) + if __name__ == '__main__': unittest.main(defaultTest='suite') From 24027d1bf8f1910fb9dfb3e7d6e76a6f2185d1af Mon Sep 17 00:00:00 2001 From: "A.L. Kleijngeld" Date: Fri, 27 Jan 2017 21:48:46 +0100 Subject: [PATCH 106/193] Update config.rst: see issue #2410 `When a match is above the *medium* recommendation threshold` -> `When a match is below the *medium* recommendation threshold` As discussed in issue #2410, which is fixed by this edit. --- docs/reference/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 8e43e2dd7..779e920d2 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -620,7 +620,7 @@ automatically accept any matches above 90% similarity, use:: The default strong recommendation threshold is 0.04. The ``medium_rec_thresh`` and ``rec_gap_thresh`` options work similarly. When a -match is above the *medium* recommendation threshold or the distance between it +match is below the *medium* recommendation threshold or the distance between it and the next-best match is above the *gap* threshold, the importer will suggest that match but not automatically confirm it. Otherwise, you'll see a list of options to choose from. From 48337870cc8318efc7a69ed230f4f2004bb237b7 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 28 Jan 2017 15:21:10 -0500 Subject: [PATCH 107/193] Bump the required discogs-client version (#2387) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1019a74a7..148643837 100755 --- a/setup.py +++ b/setup.py @@ -114,7 +114,7 @@ setup( 'absubmit': ['requests'], 'fetchart': ['requests'], 'chroma': ['pyacoustid'], - 'discogs': ['discogs-client>=2.1.0'], + 'discogs': ['discogs-client>=2.2.1'], 'beatport': ['requests-oauthlib>=0.6.1'], 'lastgenre': ['pylast'], 'mpdstats': ['python-mpd2>=0.4.2'], From 7c79d8ce89ff1ee9096ba0d12847e0999764c08e Mon Sep 17 00:00:00 2001 From: wordofglass Date: Mon, 30 Jan 2017 10:01:19 +0100 Subject: [PATCH 108/193] fetchart: minor restructuring of tests. --- test/test_art.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_art.py b/test/test_art.py index d47b280c2..6b0e5bbc5 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -43,7 +43,9 @@ class Settings(): """Used to pass settings to the ArtSources when the plugin isn't fully instantiated. """ - pass + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) class UseThePlugin(_common.TestCase): @@ -80,8 +82,7 @@ class FetchImageTest(FetchImageHelper, UseThePlugin): super(FetchImageTest, self).setUp() self.dpath = os.path.join(self.temp_dir, b'arttest') self.source = fetchart.RemoteArtSource(logger, self.plugin.config) - self.settings = Settings() - self.settings.maxwidth = 0 + self.settings = Settings(maxwidth=0) self.candidate = fetchart.Candidate(logger, url=self.URL) def test_invalid_type_returns_none(self): @@ -114,9 +115,8 @@ class FSArtTest(UseThePlugin): os.mkdir(self.dpath) self.source = fetchart.FileSystem(logger, self.plugin.config) - self.settings = Settings() - self.settings.cautious = False - self.settings.cover_names = ('art',) + self.settings = Settings(cautious=False, + cover_names=('art',)) def test_finds_jpg_in_directory(self): _common.touch(os.path.join(self.dpath, b'a.jpg')) From 8638157e17c74ef54bd319958151f972317be852 Mon Sep 17 00:00:00 2001 From: Jeffrey Aylesworth Date: Mon, 30 Jan 2017 23:37:01 -0500 Subject: [PATCH 109/193] Change license and email address in mbcollection plugin. --- beetsplug/mbcollection.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index a590a7605..909e6a0a5 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011, Jeffrey Aylesworth +# This file is part of beets. +# Copyright (c) 2011, Jeffrey Aylesworth # -# Permission to use, copy, modify, and/or distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: # -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. from __future__ import division, absolute_import, print_function From 707796e60997fb4f4ce14e315b423e324d6a407f Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 1 Feb 2017 00:36:37 -0500 Subject: [PATCH 110/193] Update Windows registry file for Python 3.6 Closes #2424. --- extra/beets.reg | Bin 434 -> 464 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/extra/beets.reg b/extra/beets.reg index 30fb6e0e4dd1a1879fef9f6108a66cac9337588d..c02303d3d896c404e45cc5fa6e0038e70d32db6d 100644 GIT binary patch delta 55 zcmdnQe1UmG2%~lpLq0<~LlHwFLoR~?gBwF8Lk>eKLoq`P5C$+*GL$f609E8M7&Dkn IPGk%O0B!>eJpcdz delta 25 gcmcb>yoq^32qSMLLkU9$Lq0 Date: Sat, 4 Feb 2017 13:42:24 -0600 Subject: [PATCH 111/193] badfiles: Better logging and error handling (#2428) --- beetsplug/badfiles.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index 5ab0ae983..64b75ee3b 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -44,10 +44,11 @@ class BadFiles(BeetsPlugin): status = e.returncode except OSError as e: if e.errno == errno.ENOENT: - ui.print_(u"command not found: {}".format(cmd[0])) - sys.exit(1) + raise ui.UserError(u"command not found: {}".format(cmd[0])) else: - raise + raise ui.UserError( + u"error invoking {}: {}".format(cmd[0], e) + ) output = output.decode(sys.getfilesystemencoding()) return status, errors, [line for line in output.split("\n") if line] @@ -96,6 +97,7 @@ class BadFiles(BeetsPlugin): ext = os.path.splitext(item.path)[1][1:] checker = self.get_checker(ext) if not checker: + self._log.debug(u"no checker available for {}", ext) continue path = item.path if not isinstance(path, six.text_type): From 0216ef294a9151e1971f5bb590ca88753dd47dfb Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 4 Feb 2017 13:43:23 -0600 Subject: [PATCH 112/193] badfiles: Python 3 compatibility Discovered while working on #2428 that we were using an implicit bytes-to-str conversion on the extension. This broke dict lookup on Python 3. --- beetsplug/badfiles.py | 2 +- docs/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index 64b75ee3b..368b6eba0 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -94,7 +94,7 @@ class BadFiles(BeetsPlugin): ui.colorize('text_error', dpath))) # Run the checker against the file if one is found - ext = os.path.splitext(item.path)[1][1:] + ext = os.path.splitext(item.path)[1][1:].decode('utf8', 'ignore') checker = self.get_checker(ext) if not checker: self._log.debug(u"no checker available for {}", ext) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3ab7940e2..50a2ad518 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -32,6 +32,7 @@ Fixes: * :doc:`/plugins/mpdstats`: Fix Python 3 compatibility. The plugin also now requires version 0.4.2 or later of the ``python-mpd2`` library. :bug:`2405` * :doc:`/plugins/mpdstats`: Improve handling of mpd status queries. +* :doc:`/plugins/badfiles`: Fix Python 3 compatibility. 1.4.3 (January 9, 2017) From cb7c6bfb69a0d01a06f1d444f1518c0b1e00a34c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 4 Feb 2017 14:16:11 -0600 Subject: [PATCH 113/193] Use free-form MP4 tag for album gain (#2426) I'm not sure how this got messed up, but this was: - Trying to store album gain in the track peak part of the SoundCheck tag! - Not writing the album gain to the non-SC free-form RG tag! Together, this led to serious weirdness when writing these fields on AAC files. To be clear, we currently *only* support track-level data for SoundCheck. We also record album-level RG information in non-iTunes tags, but that's separate. A little googling suggests that SoundCheck now has album-level data, but supporting that is a separate issue. --- beets/mediafile.py | 6 +++--- docs/changelog.rst | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index c910c9a86..13f1b2dfb 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1941,9 +1941,9 @@ class MediaFile(object): u'replaygain_album_gain', float_places=2, suffix=u' dB' ), - MP4SoundCheckStorageStyle( - '----:com.apple.iTunes:iTunNORM', - index=1 + MP4StorageStyle( + '----:com.apple.iTunes:replaygain_album_gain', + float_places=2, suffix=' dB' ), StorageStyle( u'REPLAYGAIN_ALBUM_GAIN', diff --git a/docs/changelog.rst b/docs/changelog.rst index 50a2ad518..3ec604a55 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -33,6 +33,8 @@ Fixes: requires version 0.4.2 or later of the ``python-mpd2`` library. :bug:`2405` * :doc:`/plugins/mpdstats`: Improve handling of mpd status queries. * :doc:`/plugins/badfiles`: Fix Python 3 compatibility. +* Fix some cases where album-level ReplayGain/SoundCheck metadata would be + written to files incorrectly. :bug:`2426` 1.4.3 (January 9, 2017) From 88d26e76944c66c7f6bd490f4e5269bd37d700a7 Mon Sep 17 00:00:00 2001 From: karpinski Date: Mon, 6 Feb 2017 18:26:15 +0100 Subject: [PATCH 114/193] Adding a move option to the importer's CLI and updating the docs. --- beets/ui/commands.py | 4 ++++ docs/changelog.rst | 3 +++ docs/guides/tagger.rst | 3 +++ docs/reference/cli.rst | 3 ++- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 168f0d515..8a07f6147 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -941,6 +941,10 @@ import_cmd.parser.add_option( u'-C', u'--nocopy', action='store_false', dest='copy', help=u"don't copy tracks (opposite of -c)" ) +import_cmd.parser.add_option( + u'-m', u'--move', action='store_true', dest='move', + help=u"move tracks into the library (overrides -c)" +) import_cmd.parser.add_option( u'-w', u'--write', action='store_true', default=None, help=u"write new metadata to files' tags (default)" diff --git a/docs/changelog.rst b/docs/changelog.rst index 3ec604a55..c78a6fc21 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,9 @@ New features: value. The argument can be any two characters that represent the left and right brackets. It defaults to `[]` and can also be blank to turn off bracketing. :bug:`2397` :bug:`2399` +* Added a ``--move`` or ``-m`` option to the importer so that the files can be + moved to the library instead of being copied or added "in place". + :bug:`2252` :bug:`2429` Fixes: diff --git a/docs/guides/tagger.rst b/docs/guides/tagger.rst index 6c0e44f9e..4c9df42f8 100644 --- a/docs/guides/tagger.rst +++ b/docs/guides/tagger.rst @@ -95,6 +95,9 @@ command-line options you should know: * ``beet import -C``: don't copy imported files to your music directory; leave them where they are +* ``beet import -m``: move imported files to your music directory (overrides + the ``-c`` option) + * ``beet import -l LOGFILE``: write a message to ``LOGFILE`` every time you skip an album or choose to take its tags "as-is" (see below) or the album is skipped as a duplicate; this lets you come back later and reexamine albums diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 92ddc14d0..59e2eeb68 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -72,7 +72,8 @@ box. To extract `rar` files, install the `rarfile`_ package and the Optional command flags: * By default, the command copies files your the library directory and - updates the ID3 tags on your music. If you'd like to leave your music + updates the ID3 tags on your music. In order to move the files, instead of + copying, use the ``-m`` (move) option. If you'd like to leave your music files untouched, try the ``-C`` (don't copy) and ``-W`` (don't write tags) options. You can also disable this behavior by default in the configuration file (below). From ffca8f549fde5904d333613d84e8f28d7d83190d Mon Sep 17 00:00:00 2001 From: karpinski Date: Wed, 8 Feb 2017 18:52:49 +0100 Subject: [PATCH 115/193] Allowing the execution to continue to other files if validator is not found or exits with an error. --- beetsplug/badfiles.py | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index 368b6eba0..b02923dca 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -30,6 +30,24 @@ import sys import six +class CheckerCommandException(Exception): + """Raised when running a checker failed. + + Attributes: + checker: Checker command name. + path: Path to the file being validated. + errno: Error number from the checker execution error. + msg: Message from the checker execution error. + + """ + + def __init__(self, cmd, oserror): + self.checker = cmd[0] + self.path = cmd[-1] + self.errno = oserror.errno + self.msg = str(oserror) + + class BadFiles(BeetsPlugin): def run_command(self, cmd): self._log.debug(u"running command: {}", @@ -43,12 +61,7 @@ class BadFiles(BeetsPlugin): errors = 1 status = e.returncode except OSError as e: - if e.errno == errno.ENOENT: - raise ui.UserError(u"command not found: {}".format(cmd[0])) - else: - raise ui.UserError( - u"error invoking {}: {}".format(cmd[0], e) - ) + raise CheckerCommandException(cmd, e) output = output.decode(sys.getfilesystemencoding()) return status, errors, [line for line in output.split("\n") if line] @@ -97,12 +110,24 @@ class BadFiles(BeetsPlugin): ext = os.path.splitext(item.path)[1][1:].decode('utf8', 'ignore') checker = self.get_checker(ext) if not checker: - self._log.debug(u"no checker available for {}", ext) + self._log.debug(u"no checker specified in the config for {}", + ext) continue path = item.path if not isinstance(path, six.text_type): path = item.path.decode(sys.getfilesystemencoding()) - status, errors, output = checker(path) + try: + status, errors, output = checker(path) + except CheckerCommandException as e: + if e.errno == errno.ENOENT: + self._log.error( + u"command not found: {} when validating file: {}", + e.checker, + e.path + ) + else: + self._log.error(u"error invoking {}: {}", e.checker, e.msg) + continue if status > 0: ui.print_(u"{}: checker exited withs status {}" .format(ui.colorize('text_error', dpath), status)) From 291803d49ac4ce7b0af2335c6c97132b3e78a317 Mon Sep 17 00:00:00 2001 From: karpinski Date: Wed, 8 Feb 2017 18:54:18 +0100 Subject: [PATCH 116/193] Fixing a small typo. --- beetsplug/badfiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index b02923dca..beb8942a5 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -129,7 +129,7 @@ class BadFiles(BeetsPlugin): self._log.error(u"error invoking {}: {}", e.checker, e.msg) continue if status > 0: - ui.print_(u"{}: checker exited withs status {}" + ui.print_(u"{}: checker exited with status {}" .format(ui.colorize('text_error', dpath), status)) for line in output: ui.print_(u" {}".format(displayable_path(line))) From b46fb956b76d559c7ce1b7ef912e28edb5c71e9d Mon Sep 17 00:00:00 2001 From: karpinski Date: Thu, 9 Feb 2017 11:31:26 +0100 Subject: [PATCH 117/193] Making logging level consistent when checker is not found. --- beetsplug/badfiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index beb8942a5..ae378ed20 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -110,7 +110,7 @@ class BadFiles(BeetsPlugin): ext = os.path.splitext(item.path)[1][1:].decode('utf8', 'ignore') checker = self.get_checker(ext) if not checker: - self._log.debug(u"no checker specified in the config for {}", + self._log.error(u"no checker specified in the config for {}", ext) continue path = item.path From 7e6e043f2dbbd3aaa623e3b682a4a539dc5f2971 Mon Sep 17 00:00:00 2001 From: karpinski Date: Fri, 10 Feb 2017 12:53:24 +0100 Subject: [PATCH 118/193] Adding a changelog entry. --- docs/changelog.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c78a6fc21..77b1a61cc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -38,7 +38,9 @@ Fixes: * :doc:`/plugins/badfiles`: Fix Python 3 compatibility. * Fix some cases where album-level ReplayGain/SoundCheck metadata would be written to files incorrectly. :bug:`2426` - +* Fixed the badfiles plugin to allow the execution to continue to other files + if validator command is not found or exists with an error. :bug:`2430` + :bug:`2433` 1.4.3 (January 9, 2017) ----------------------- From 8ef9f68843a0c7a3b6da0a02b807c7e4893209d1 Mon Sep 17 00:00:00 2001 From: Marcin Karpinski Date: Fri, 10 Feb 2017 22:51:23 +0100 Subject: [PATCH 119/193] badfiles: only output corrupt files by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit badfiles now only outputs corrupt files by default — to get full output enable verbose mode using -v or --verbose --- beetsplug/badfiles.py | 7 ++++++- docs/changelog.rst | 3 +++ docs/plugins/badfiles.rst | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index 368b6eba0..40a3ef7a8 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -113,11 +113,16 @@ class BadFiles(BeetsPlugin): .format(ui.colorize('text_warning', dpath), errors)) for line in output: ui.print_(u" {}".format(displayable_path(line))) - else: + elif opts.verbose: ui.print_(u"{}: ok".format(ui.colorize('text_success', dpath))) def commands(self): bad_command = Subcommand('bad', help=u'check for corrupt or missing files') + bad_command.parser.add_option( + u'-v', u'--verbose', + action='store_true', default=False, dest='verbose', + help=u'view results for both the bad and uncorrupted files' + ) bad_command.func = self.check_bad return [bad_command] diff --git a/docs/changelog.rst b/docs/changelog.rst index c78a6fc21..af03df646 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,6 +25,9 @@ New features: * Added a ``--move`` or ``-m`` option to the importer so that the files can be moved to the library instead of being copied or added "in place". :bug:`2252` :bug:`2429` +* Added a ``--verbose`` or ``-v`` option to the badfiles plugin. Results are + now displayed only for corrupted files by default and for all the files when + the verbose option is set. :bug:`1654` :bug:`2434` Fixes: diff --git a/docs/plugins/badfiles.rst b/docs/plugins/badfiles.rst index 80f543c54..0a32f1a36 100644 --- a/docs/plugins/badfiles.rst +++ b/docs/plugins/badfiles.rst @@ -52,3 +52,7 @@ Note that the default `mp3val` checker is a bit verbose and can output a lot of "stream error" messages, even for files that play perfectly well. Generally, if more than one stream error happens, or if a stream error happens in the middle of a file, this is a bad sign. + +By default, only errors for the bad files will be shown. In order for the +results for all of the checked files to be seen, including the uncorrupted +ones, use the ``-v`` or ``--verbose`` option. From 2fcb68afd885b326eefe06150e24462b7c24e45e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 11 Feb 2017 19:23:39 -0500 Subject: [PATCH 120/193] Little changelog fixes (#2433) --- docs/changelog.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5fd388e4f..1d191383e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,8 +24,8 @@ New features: bracketing. :bug:`2397` :bug:`2399` * Added a ``--move`` or ``-m`` option to the importer so that the files can be moved to the library instead of being copied or added "in place". - :bug:`2252` :bug:`2429` -* Added a ``--verbose`` or ``-v`` option to the badfiles plugin. Results are + :bug:`2252` :bug:`2429` +* :doc:`/plugins/badfiles`: Added a ``--verbose`` or ``-v`` option. Results are now displayed only for corrupted files by default and for all the files when the verbose option is set. :bug:`1654` :bug:`2434` @@ -41,9 +41,9 @@ Fixes: * :doc:`/plugins/badfiles`: Fix Python 3 compatibility. * Fix some cases where album-level ReplayGain/SoundCheck metadata would be written to files incorrectly. :bug:`2426` -* Fixed the badfiles plugin to allow the execution to continue to other files - if validator command is not found or exists with an error. :bug:`2430` - :bug:`2433` +* :doc:`/plugins/badfiles`: The command no longer bails out if validator + command is not found or exists with an error. :bug:`2430` :bug:`2433` + 1.4.3 (January 9, 2017) ----------------------- From 86c8cffa6c218ddb5499f28f22a273cb26d5109f Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 11 Feb 2017 19:24:47 -0500 Subject: [PATCH 121/193] Fix some whitespace (#2433) --- beetsplug/badfiles.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index 1d9396875..62c6d8af5 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -32,13 +32,12 @@ import six class CheckerCommandException(Exception): """Raised when running a checker failed. - + Attributes: checker: Checker command name. path: Path to the file being validated. errno: Error number from the checker execution error. msg: Message from the checker execution error. - """ def __init__(self, cmd, oserror): From 8087e828914a917a864dad32bd4ce06ed286b527 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 12 Feb 2017 10:30:22 -0500 Subject: [PATCH 122/193] lyrics: Use Requests for Google backend (fix #2437) --- beetsplug/lyrics.py | 4 ++-- docs/changelog.rst | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 92abe377e..c020a93ae 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -563,8 +563,8 @@ class Google(Backend): % (self.api_key, self.engine_id, urllib.parse.quote(query.encode('utf-8'))) - data = urllib.request.urlopen(url) - data = json.load(data) + data = self.fetch_url(url) + data = json.loads(data) if 'error' in data: reason = data['error']['errors'][0]['reason'] self._log.debug(u'google lyrics backend error: {0}', reason) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1d191383e..0774722c8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -43,6 +43,8 @@ Fixes: written to files incorrectly. :bug:`2426` * :doc:`/plugins/badfiles`: The command no longer bails out if validator command is not found or exists with an error. :bug:`2430` :bug:`2433` +* :doc:`/plugins/lyrics`: The Google search backend no longer crashes when the + server responds with an error. :bug:`2437` 1.4.3 (January 9, 2017) From 3361aaafdaead7eaeac206decaf6c21331ef556d Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 31 Jan 2017 02:15:29 -0800 Subject: [PATCH 123/193] =?UTF-8?q?Embedart=20plugin=20asks=20for=20confir?= =?UTF-8?q?mation=20before=20making=20changes=20to=20item=E2=80=99s=20artw?= =?UTF-8?q?ork.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- beetsplug/embedart.py | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 948da6291..ff65e34db 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -20,7 +20,7 @@ import os.path from beets.plugins import BeetsPlugin from beets import ui -from beets.ui import decargs +from beets.ui import print_, decargs from beets.util import syspath, normpath, displayable_path, bytestring_path from beets.util.artresizer import ArtResizer from beets import config @@ -60,10 +60,28 @@ class EmbedCoverArtPlugin(BeetsPlugin): embed_cmd.parser.add_option( u'-f', u'--file', metavar='PATH', help=u'the image file to embed' ) + embed_cmd.parser.add_option( + u"-y", u"--yes", action="store_true", help=u"skip confirmation" + ) maxwidth = self.config['maxwidth'].get(int) compare_threshold = self.config['compare_threshold'].get(int) ifempty = self.config['ifempty'].get(bool) + def _confirmation(items, opts, fmt, prompt): + # Confirm artwork changes to library items. + if not opts.yes: + # Prepare confirmation with user. + print_() + + # Show all the items. + for item in items: + print_(format(item, fmt)) + + # Confirm with user. + if not ui.input_yn(prompt, True): + return False + return True + def embed_func(lib, opts, args): if opts.file: imagepath = normpath(opts.file) @@ -71,11 +89,30 @@ class EmbedCoverArtPlugin(BeetsPlugin): raise ui.UserError(u'image file {0} not found'.format( displayable_path(imagepath) )) - for item in lib.items(decargs(args)): + + items = lib.items(decargs(args)) + + # Confirm with user. + fmt = u'$albumartist - $album - $title' + prompt = u'Modify artwork for %i file%s (y/n)?' % \ + (len(items), 's' if len(items) > 1 else '') + if not _confirmation(items, opts, fmt, prompt): + return + + for item in items: art.embed_item(self._log, item, imagepath, maxwidth, None, compare_threshold, ifempty) else: - for album in lib.albums(decargs(args)): + items = lib.albums(decargs(args)) + + # Confirm with user. + fmt = u'$albumartist - $album' + prompt = u'Modify artwork for %i album%s (y/n)?' % \ + (len(items), 's' if len(items) > 1 else '') + if not _confirmation(items, opts, fmt, prompt): + return + + for album in items: art.embed_album(self._log, album, maxwidth, False, compare_threshold, ifempty) self.remove_artfile(album) From 9c97f95073c026114ef7d80b8a549ba7fed63452 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 1 Feb 2017 03:38:38 -0800 Subject: [PATCH 124/193] Updated embedart test cases to accomodate confirmation prompt. --- test/test_embedart.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/test_embedart.py b/test/test_embedart.py index ee08ecb4e..5360ea372 100644 --- a/test/test_embedart.py +++ b/test/test_embedart.py @@ -51,6 +51,9 @@ class EmbedartCliTest(_common.TestCase, TestHelper): abbey_differentpath = os.path.join(_common.RSRC, b'abbey-different.jpg') def setUp(self): + super(EmbedartCliTest, self).setUp() + self.io.install() + self.setup_beets() # Converter is threaded self.load_plugins('embedart') @@ -68,6 +71,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self._setup_data() album = self.add_album_fixture() item = album.items()[0] + self.io.addinput('y') self.run_command('embedart', '-f', self.small_artpath) mediafile = MediaFile(syspath(item.path)) self.assertEqual(mediafile.images[0].data, self.image_data) @@ -78,6 +82,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): item = album.items()[0] album.artpath = self.small_artpath album.store() + self.io.addinput('y') self.run_command('embedart') mediafile = MediaFile(syspath(item.path)) self.assertEqual(mediafile.images[0].data, self.image_data) @@ -96,6 +101,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): album.store() config['embedart']['remove_art_file'] = True + self.io.addinput('y') self.run_command('embedart') if os.path.isfile(tmp_path): @@ -106,6 +112,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self.add_album_fixture() logging.getLogger('beets.embedart').setLevel(logging.DEBUG) with self.assertRaises(ui.UserError): + self.io.addinput('y') self.run_command('embedart', '-f', '/doesnotexist') def test_embed_non_image_file(self): @@ -117,6 +124,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): os.close(handle) try: + self.io.addinput('y') self.run_command('embedart', '-f', tmp_path) finally: os.remove(tmp_path) @@ -129,8 +137,10 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self._setup_data(self.abbey_artpath) album = self.add_album_fixture() item = album.items()[0] + self.io.addinput('y') self.run_command('embedart', '-f', self.abbey_artpath) config['embedart']['compare_threshold'] = 20 + self.io.addinput('y') self.run_command('embedart', '-f', self.abbey_differentpath) mediafile = MediaFile(syspath(item.path)) @@ -143,9 +153,10 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self._setup_data(self.abbey_similarpath) album = self.add_album_fixture() item = album.items()[0] + self.io.addinput('y') self.run_command('embedart', '-f', self.abbey_artpath) config['embedart']['compare_threshold'] = 20 - self.run_command('embedart', '-f', self.abbey_similarpath) + self.run_command('embedart', '-y', '-f', self.abbey_similarpath) mediafile = MediaFile(syspath(item.path)) self.assertEqual(mediafile.images[0].data, self.image_data, @@ -169,7 +180,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): trackpath = album.items()[0].path albumpath = album.path shutil.copy(syspath(resource_path), syspath(trackpath)) - + self.run_command('extractart', '-n', 'extracted') self.assertExists(os.path.join(albumpath, b'extracted.jpg')) From d1ac8939155dab36b03a757c7e6c3b73fd35b634 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 1 Feb 2017 14:32:52 -0800 Subject: [PATCH 125/193] Style changes to pass PEP8 tests. --- beetsplug/embedart.py | 2 +- test/test_embedart.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index ff65e34db..056c8f36a 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -95,7 +95,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): # Confirm with user. fmt = u'$albumartist - $album - $title' prompt = u'Modify artwork for %i file%s (y/n)?' % \ - (len(items), 's' if len(items) > 1 else '') + (len(items), 's' if len(items) > 1 else '') if not _confirmation(items, opts, fmt, prompt): return diff --git a/test/test_embedart.py b/test/test_embedart.py index 5360ea372..b0e66dddb 100644 --- a/test/test_embedart.py +++ b/test/test_embedart.py @@ -180,7 +180,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): trackpath = album.items()[0].path albumpath = album.path shutil.copy(syspath(resource_path), syspath(trackpath)) - + self.run_command('extractart', '-n', 'extracted') self.assertExists(os.path.join(albumpath, b'extracted.jpg')) From c250a53b4d8073f0346c82eda66c1d47a9a86999 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 1 Feb 2017 17:04:01 -0800 Subject: [PATCH 126/193] Updated changelog and embedart docs. --- docs/changelog.rst | 2 ++ docs/plugins/embedart.rst | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0774722c8..0c3779c3c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,6 +28,8 @@ New features: * :doc:`/plugins/badfiles`: Added a ``--verbose`` or ``-v`` option. Results are now displayed only for corrupted files by default and for all the files when the verbose option is set. :bug:`1654` :bug:`2434` + * :doc:`/plugins/embedart` by default now asks for confirmation before + embedding art into music files. Thanks to :user:`Stunner`. :bug:`1999` Fixes: diff --git a/docs/plugins/embedart.rst b/docs/plugins/embedart.rst index 94e91d995..717bfb8e2 100644 --- a/docs/plugins/embedart.rst +++ b/docs/plugins/embedart.rst @@ -81,7 +81,8 @@ embedded album art: * ``beet embedart [-f IMAGE] QUERY``: embed images into the every track on the albums matching the query. If the ``-f`` (``--file``) option is given, then use a specific image file from the filesystem; otherwise, each album embeds - its own currently associated album art. + its own currently associated album art. Will prompt for confirmation before + making the change unless the ``-y`` (``--yes``) option is specified. * ``beet extractart [-a] [-n FILE] QUERY``: extracts the images for all albums matching the query. The images are placed inside the album folder. You can From 3e13971c545d376ec6ad555231f6dd9faee4eb8e Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 13 Feb 2017 02:30:01 -0800 Subject: [PATCH 127/193] Some code cleanup/consolidation. --- beetsplug/embedart.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 056c8f36a..21fb0188c 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -67,12 +67,20 @@ class EmbedCoverArtPlugin(BeetsPlugin): compare_threshold = self.config['compare_threshold'].get(int) ifempty = self.config['ifempty'].get(bool) - def _confirmation(items, opts, fmt, prompt): + def _confirmation(items, opts): # Confirm artwork changes to library items. if not opts.yes: # Prepare confirmation with user. print_() + fmt = u'$albumartist - $album' + item_type = u'album' + if opts.file: + fmt = u'$albumartist - $album - $title' + item_type = u'file' + prompt = u'Modify artwork for %i %s%s (y/n)?' % \ + (len(items), item_type, 's' if len(items) > 1 else '') + # Show all the items. for item in items: print_(format(item, fmt)) @@ -93,10 +101,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): items = lib.items(decargs(args)) # Confirm with user. - fmt = u'$albumartist - $album - $title' - prompt = u'Modify artwork for %i file%s (y/n)?' % \ - (len(items), 's' if len(items) > 1 else '') - if not _confirmation(items, opts, fmt, prompt): + if not _confirmation(items, opts): return for item in items: @@ -106,10 +111,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): items = lib.albums(decargs(args)) # Confirm with user. - fmt = u'$albumartist - $album' - prompt = u'Modify artwork for %i album%s (y/n)?' % \ - (len(items), 's' if len(items) > 1 else '') - if not _confirmation(items, opts, fmt, prompt): + if not _confirmation(items, opts): return for album in items: From 733c1839fb3e67dc79f5ad707d993efc9676194d Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 13 Feb 2017 02:43:13 -0800 Subject: [PATCH 128/193] Addressed coding style issue. --- beetsplug/embedart.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 21fb0188c..6344dc2b9 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -74,12 +74,12 @@ class EmbedCoverArtPlugin(BeetsPlugin): print_() fmt = u'$albumartist - $album' - item_type = u'album' + istr = u'album' if opts.file: fmt = u'$albumartist - $album - $title' - item_type = u'file' + istr = u'file' prompt = u'Modify artwork for %i %s%s (y/n)?' % \ - (len(items), item_type, 's' if len(items) > 1 else '') + (len(items), istr, 's' if len(items) > 1 else '') # Show all the items. for item in items: From 0a4709f7ef231ff6499f8ef24aeaa5017ce8d07c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 13 Feb 2017 16:54:56 -0500 Subject: [PATCH 129/193] lyrics: Tolerate empty Google response (#2437) --- beetsplug/lyrics.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index c020a93ae..6714b2fee 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -564,11 +564,17 @@ class Google(Backend): urllib.parse.quote(query.encode('utf-8'))) data = self.fetch_url(url) - data = json.loads(data) + if not data: + self._log.debug(u'google backend returned no data') + return None + try: + data = json.loads(data) + except ValueError as exc: + self._log.debug(u'google backend returned malformed JSON: {}', exc) if 'error' in data: reason = data['error']['errors'][0]['reason'] - self._log.debug(u'google lyrics backend error: {0}', reason) - return + self._log.debug(u'google backend error: {0}', reason) + return None if 'items' in data.keys(): for item in data['items']: From b4efecb709598b5a8bd362d9a1bdf8d9f2e272a5 Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Sun, 19 Feb 2017 15:56:13 -0600 Subject: [PATCH 130/193] Add option to hardlink when importing --- beets/importer.py | 17 ++++++++++++----- beets/library.py | 26 +++++++++++++++++--------- beets/util/__init__.py | 26 ++++++++++++++++++++++++++ beetsplug/importadded.py | 3 ++- docs/dev/plugins.rst | 4 ++++ docs/reference/config.rst | 19 +++++++++++++++++-- test/_common.py | 1 + test/test_files.py | 17 +++++++++++++++++ test/test_importadded.py | 1 + test/test_importer.py | 23 ++++++++++++++++++++++- test/test_mediafile_edge.py | 9 +++++++++ 11 files changed, 128 insertions(+), 18 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index 6a10f4c97..275e889f6 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -220,13 +220,19 @@ class ImportSession(object): iconfig['resume'] = False iconfig['incremental'] = False - # Copy, move, and link are mutually exclusive. + # Copy, move, link, and hardlink are mutually exclusive. if iconfig['move']: iconfig['copy'] = False iconfig['link'] = False + iconfig['hardlink'] = False elif iconfig['link']: iconfig['copy'] = False iconfig['move'] = False + iconfig['hardlink'] = False + elif iconfig['hardlink']: + iconfig['copy'] = False + iconfig['move'] = False + iconfig['link'] = False # Only delete when copying. if not iconfig['copy']: @@ -654,18 +660,18 @@ class ImportTask(BaseImportTask): item.update(changes) def manipulate_files(self, move=False, copy=False, write=False, - link=False, session=None): + link=False, hardlink=False, session=None): items = self.imported_items() # Save the original paths of all items for deletion and pruning # in the next step (finalization). self.old_paths = [item.path for item in items] for item in items: - if move or copy or link: + if move or copy or link or hardlink: # In copy and link modes, treat re-imports specially: # move in-library files. (Out-of-library files are # copied/moved as usual). old_path = item.path - if (copy or link) and self.replaced_items[item] and \ + if (copy or link or hardlink) and self.replaced_items[item] and \ session.lib.directory in util.ancestry(old_path): item.move() # We moved the item, so remove the @@ -674,7 +680,7 @@ class ImportTask(BaseImportTask): else: # A normal import. Just copy files and keep track of # old paths. - item.move(copy, link) + item.move(copy, link, hardlink) if write and (self.apply or self.choice_flag == action.RETAG): item.try_write() @@ -1412,6 +1418,7 @@ def manipulate_files(session, task): copy=session.config['copy'], write=session.config['write'], link=session.config['link'], + hardlink=session.config['hardlink'], session=session, ) diff --git a/beets/library.py b/beets/library.py index 4e5d9ccf6..b263ecd64 100644 --- a/beets/library.py +++ b/beets/library.py @@ -663,7 +663,7 @@ class Item(LibModel): # Files themselves. - def move_file(self, dest, copy=False, link=False): + def move_file(self, dest, copy=False, link=False, hardlink=False): """Moves or copies the item's file, updating the path value if the move succeeds. If a file exists at ``dest``, then it is slightly modified to be unique. @@ -678,6 +678,10 @@ class Item(LibModel): util.link(self.path, dest) plugins.send("item_linked", item=self, source=self.path, destination=dest) + elif hardlink: + util.hardlink(self.path, dest) + plugins.send("item_hardlinked", item=self, source=self.path, + destination=dest) else: plugins.send("before_item_moved", item=self, source=self.path, destination=dest) @@ -730,15 +734,16 @@ class Item(LibModel): self._db._memotable = {} - def move(self, copy=False, link=False, basedir=None, with_album=True, - store=True): + def move(self, copy=False, link=False, hardlink=False, basedir=None, + with_album=True, store=True): """Move the item to its designated location within the library directory (provided by destination()). Subdirectories are created as needed. If the operation succeeds, the item's path field is updated to reflect the new location. If `copy` is true, moving the file is copied rather than moved. - Similarly, `link` creates a symlink instead. + Similarly, `link` creates a symlink instead, and `hardlink` + creates a hardlink. basedir overrides the library base directory for the destination. @@ -761,7 +766,7 @@ class Item(LibModel): # Perform the move and store the change. old_path = self.path - self.move_file(dest, copy, link) + self.move_file(dest, copy, link, hardlink) if store: self.store() @@ -979,7 +984,7 @@ class Album(LibModel): for item in self.items(): item.remove(delete, False) - def move_art(self, copy=False, link=False): + def move_art(self, copy=False, link=False, hardlink=False): """Move or copy any existing album art so that it remains in the same directory as the items. """ @@ -999,6 +1004,8 @@ class Album(LibModel): util.copy(old_art, new_art) elif link: util.link(old_art, new_art) + elif hardlink: + util.hardlink(old_art, new_art) else: util.move(old_art, new_art) self.artpath = new_art @@ -1008,7 +1015,8 @@ class Album(LibModel): util.prune_dirs(os.path.dirname(old_art), self._db.directory) - def move(self, copy=False, link=False, basedir=None, store=True): + def move(self, copy=False, link=False, hardlink=False, basedir=None, + store=True): """Moves (or copies) all items to their destination. Any album art moves along with them. basedir overrides the library base directory for the destination. By default, the album is stored to the @@ -1026,11 +1034,11 @@ class Album(LibModel): # Move items. items = list(self.items()) for item in items: - item.move(copy, link, basedir=basedir, with_album=False, + item.move(copy, link, hardlink, basedir=basedir, with_album=False, store=store) # Move art. - self.move_art(copy, link) + self.move_art(copy, link, hardlink) if store: self.store() diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 18d89ddd9..ba78dfa30 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -500,6 +500,32 @@ def link(path, dest, replace=False): traceback.format_exc()) +def hardlink(path, dest, replace=False): + """Create a hard link from path to `dest`. Raises an OSError if + `dest` already exists, unless `replace` is True. Does nothing if + `path` == `dest`.""" + if (samefile(path, dest)): + return + + path = syspath(path) + dest = syspath(dest) + if os.path.exists(dest) and not replace: + raise FilesystemError(u'file exists', 'rename', (path, dest)) + try: + os.link(path, dest) + except NotImplementedError: + # raised on python >= 3.2 and Windows versions before Vista + raise FilesystemError(u'OS does not support hard links.' + 'link', (path, dest), traceback.format_exc()) + except OSError as exc: + # TODO: Windows version checks can be removed for python 3 + if hasattr('sys', 'getwindowsversion'): + if sys.getwindowsversion()[0] < 6: # is before Vista + exc = u'OS does not support hard links.' + raise FilesystemError(exc, 'link', (path, dest), + traceback.format_exc()) + + def unique_path(path): """Returns a version of ``path`` that does not exist on the filesystem. Specifically, if ``path` itself already exists, then diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index 07434ee72..c1838884b 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -36,6 +36,7 @@ class ImportAddedPlugin(BeetsPlugin): register('before_item_moved', self.record_import_mtime) register('item_copied', self.record_import_mtime) register('item_linked', self.record_import_mtime) + register('item_hardlinked', self.record_import_mtime) register('album_imported', self.update_album_times) register('item_imported', self.update_item_times) register('after_write', self.update_after_write_time) @@ -51,7 +52,7 @@ class ImportAddedPlugin(BeetsPlugin): def record_if_inplace(self, task, session): if not (session.config['copy'] or session.config['move'] or - session.config['link']): + session.config['link'] or session.config['hardlink']): self._log.debug(u"In place import detected, recording mtimes from " u"source paths") items = [task.item] \ diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index fb063aee0..4d41c8971 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -161,6 +161,10 @@ The events currently available are: for a file. Parameters: ``item``, ``source`` path, ``destination`` path +* `item_hardlinked`: called with an ``Item`` object whenever a hardlink is + created for a file. + Parameters: ``item``, ``source`` path, ``destination`` path + * `item_removed`: called with an ``Item`` object every time an item (singleton or album's part) is removed from the library (even when its file is not deleted from disk). diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 779e920d2..679b036f6 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -433,8 +433,8 @@ link ~~~~ Either ``yes`` or ``no``, indicating whether to use symbolic links instead of -moving or copying files. (It conflicts with the ``move`` and ``copy`` -options.) Defaults to ``no``. +moving or copying files. (It conflicts with the ``move``, ``copy`` and +``hardlink`` options.) Defaults to ``no``. This option only works on platforms that support symbolic links: i.e., Unixes. It will fail on Windows. @@ -442,6 +442,21 @@ It will fail on Windows. It's likely that you'll also want to set ``write`` to ``no`` if you use this option to preserve the metadata on the linked files. +.. _hardlink: + +hardlink +~~~~ + +Either ``yes`` or ``no``, indicating whether to use hard links instead of +moving or copying or symlinking files. (It conflicts with the ``move``, +``copy``, and ``link`` options.) Defaults to ``no``. + +This option only works on platforms that support hardlinks: i.e., Unixes. +It will fail on Windows. + +It's likely that you'll also want to set ``write`` to ``no`` if you use this +option to preserve the metadata on the linked files. + resume ~~~~~~ diff --git a/test/_common.py b/test/_common.py index 2e7418516..f3213ec31 100644 --- a/test/_common.py +++ b/test/_common.py @@ -54,6 +54,7 @@ _item_ident = 0 # OS feature test. HAVE_SYMLINK = sys.platform != 'win32' +HAVE_HARDLINK = sys.platform != 'win32' def item(lib=None): diff --git a/test/test_files.py b/test/test_files.py index b566f363e..4a81b8c20 100644 --- a/test/test_files.py +++ b/test/test_files.py @@ -141,6 +141,23 @@ class MoveTest(_common.TestCase): self.i.move(link=True) self.assertEqual(self.i.path, util.normpath(self.dest)) + @unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks") + def test_hardlink_arrives(self): + self.i.move(hardlink=True) + self.assertExists(self.dest) + self.assertTrue(os.path.islink(self.dest)) + self.assertEqual(os.readlink(self.dest), self.path) + + @unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks") + def test_hardlink_does_not_depart(self): + self.i.move(hardlink=True) + self.assertExists(self.path) + + @unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks") + def test_hardlink_changes_path(self): + self.i.move(hardlink=True) + self.assertEqual(self.i.path, util.normpath(self.dest)) + class HelperTest(_common.TestCase): def test_ancestry_works_on_file(self): diff --git a/test/test_importadded.py b/test/test_importadded.py index 52aa26756..dd933c3c8 100644 --- a/test/test_importadded.py +++ b/test/test_importadded.py @@ -93,6 +93,7 @@ class ImportAddedTest(unittest.TestCase, ImportHelper): self.config['import']['copy'] = False self.config['import']['move'] = False self.config['import']['link'] = False + self.config['import']['hardlink'] = False self.assertAlbumImport() def test_import_album_with_preserved_mtimes(self): diff --git a/test/test_importer.py b/test/test_importer.py index 87724f8da..c372d04da 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -22,6 +22,7 @@ import re import shutil import unicodedata import sys +import stat from six import StringIO from tempfile import mkstemp from zipfile import ZipFile @@ -209,7 +210,8 @@ class ImportHelper(TestHelper): def _setup_import_session(self, import_dir=None, delete=False, threaded=False, copy=True, singletons=False, - move=False, autotag=True, link=False): + move=False, autotag=True, link=False, + hardlink=False): config['import']['copy'] = copy config['import']['delete'] = delete config['import']['timid'] = True @@ -219,6 +221,7 @@ class ImportHelper(TestHelper): config['import']['autotag'] = autotag config['import']['resume'] = False config['import']['link'] = link + config['import']['hardlink'] = hardlink self.importer = TestImportSession( self.lib, loghandler=None, query=None, @@ -353,6 +356,24 @@ class NonAutotaggedImportTest(_common.TestCase, ImportHelper): mediafile.path ) + @unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks") + def test_import_hardlink_arrives(self): + config['import']['hardlink'] = True + self.importer.run() + for mediafile in self.import_media: + filename = os.path.join( + self.libdir, + b'Tag Artist', b'Tag Album', + util.bytestring_path('{0}.mp3'.format(mediafile.title)) + ) + self.assertExists(filename) + s1 = os.stat(mediafile.path) + s2 = os.stat(filename) + self.assertTrue( + (s1[stat.ST_INO], s1[stat.ST_DEV]) == \ + (s2[stat.ST_INO], s2[stat.ST_DEV]) + ) + def create_archive(session): (handle, path) = mkstemp(dir=py3_path(session.temp_dir)) diff --git a/test/test_mediafile_edge.py b/test/test_mediafile_edge.py index 657ca455d..8b28890be 100644 --- a/test/test_mediafile_edge.py +++ b/test/test_mediafile_edge.py @@ -197,6 +197,15 @@ class SafetyTest(unittest.TestCase, _common.TempDirMixin): finally: os.unlink(fn) + @unittest.skipUnless(_common.HAVE_HARDLINK, u'platform lacks hardlink') + def test_broken_hardlink(self): + fn = os.path.join(_common.RSRC, b'brokenlink') + os.link('does_not_exist', fn) + try: + self.assertRaises(mediafile.UnreadableFileError, + mediafile.MediaFile, fn) + finally: + os.unlink(fn) class SideEffectsTest(unittest.TestCase): def setUp(self): From 076c753943e764b2e2a5707a9f9322cc73dfee61 Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Sun, 19 Feb 2017 16:54:11 -0600 Subject: [PATCH 131/193] Remove windows hardlink checks --- beets/util/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index ba78dfa30..83f35f563 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -514,14 +514,9 @@ def hardlink(path, dest, replace=False): try: os.link(path, dest) except NotImplementedError: - # raised on python >= 3.2 and Windows versions before Vista raise FilesystemError(u'OS does not support hard links.' 'link', (path, dest), traceback.format_exc()) except OSError as exc: - # TODO: Windows version checks can be removed for python 3 - if hasattr('sys', 'getwindowsversion'): - if sys.getwindowsversion()[0] < 6: # is before Vista - exc = u'OS does not support hard links.' raise FilesystemError(exc, 'link', (path, dest), traceback.format_exc()) From e5c93b6b16c0160ca86552d4a1ea07cfd295f9b5 Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Sun, 19 Feb 2017 16:57:16 -0600 Subject: [PATCH 132/193] Add hardlink: no to default config --- beets/config_default.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/beets/config_default.yaml b/beets/config_default.yaml index fa77a82dc..3b0377966 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -6,6 +6,7 @@ import: copy: yes move: no link: no + hardlink: no delete: no resume: ask incremental: no From 0c836f9369646abf781e90fcc85d951377fd1944 Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Sun, 19 Feb 2017 17:12:47 -0600 Subject: [PATCH 133/193] Add a user-friendly error for cross-device hardlinks --- beets/util/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 83f35f563..d7d45941c 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -18,6 +18,7 @@ from __future__ import division, absolute_import, print_function import os import sys +import errno import locale import re import shutil @@ -517,8 +518,12 @@ def hardlink(path, dest, replace=False): raise FilesystemError(u'OS does not support hard links.' 'link', (path, dest), traceback.format_exc()) except OSError as exc: - raise FilesystemError(exc, 'link', (path, dest), - traceback.format_exc()) + if exc.errno == errno.EXDEV: + raise FilesystemError(u'Cannot hard link across devices.' + 'link', (path, dest), traceback.format_exc()) + else: + raise FilesystemError(exc, 'link', (path, dest), + traceback.format_exc()) def unique_path(path): From ccd0f5d12933903a479edcd35205a7c79e5eb7a2 Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Sun, 19 Feb 2017 17:19:42 -0600 Subject: [PATCH 134/193] Remove not-found hardlink test (the OS prevents this from happening) --- test/test_mediafile_edge.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/test_mediafile_edge.py b/test/test_mediafile_edge.py index 8b28890be..c631cdecf 100644 --- a/test/test_mediafile_edge.py +++ b/test/test_mediafile_edge.py @@ -197,16 +197,6 @@ class SafetyTest(unittest.TestCase, _common.TempDirMixin): finally: os.unlink(fn) - @unittest.skipUnless(_common.HAVE_HARDLINK, u'platform lacks hardlink') - def test_broken_hardlink(self): - fn = os.path.join(_common.RSRC, b'brokenlink') - os.link('does_not_exist', fn) - try: - self.assertRaises(mediafile.UnreadableFileError, - mediafile.MediaFile, fn) - finally: - os.unlink(fn) - class SideEffectsTest(unittest.TestCase): def setUp(self): self.empty = os.path.join(_common.RSRC, b'empty.mp3') From 902b955696e1a4fbd214802f29f7af32d12d200c Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Sun, 19 Feb 2017 17:22:01 -0600 Subject: [PATCH 135/193] Fix test_hardlink_arrives --- test/test_files.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/test_files.py b/test/test_files.py index 4a81b8c20..38689bbd9 100644 --- a/test/test_files.py +++ b/test/test_files.py @@ -145,8 +145,12 @@ class MoveTest(_common.TestCase): def test_hardlink_arrives(self): self.i.move(hardlink=True) self.assertExists(self.dest) - self.assertTrue(os.path.islink(self.dest)) - self.assertEqual(os.readlink(self.dest), self.path) + s1 = os.stat(self.path) + s2 = os.stat(self.dest) + self.assertTrue( + (s1[stat.ST_INO], s1[stat.ST_DEV]) == \ + (s2[stat.ST_INO], s2[stat.ST_DEV]) + ) @unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks") def test_hardlink_does_not_depart(self): From 1fd22604fb50d8c53e9eb733ddba624e887225b0 Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Sun, 19 Feb 2017 17:33:26 -0600 Subject: [PATCH 136/193] Fix linter issues --- beets/importer.py | 4 ++-- docs/reference/config.rst | 2 +- test/test_files.py | 4 ++-- test/test_importer.py | 4 ++-- test/test_mediafile_edge.py | 1 + 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index 275e889f6..bbe152cd4 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -671,8 +671,8 @@ class ImportTask(BaseImportTask): # move in-library files. (Out-of-library files are # copied/moved as usual). old_path = item.path - if (copy or link or hardlink) and self.replaced_items[item] and \ - session.lib.directory in util.ancestry(old_path): + if (copy or link or hardlink) and self.replaced_items[item] \ + and session.lib.directory in util.ancestry(old_path): item.move() # We moved the item, so remove the # now-nonexistent file from old_paths. diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 679b036f6..bf3d89b2e 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -445,7 +445,7 @@ option to preserve the metadata on the linked files. .. _hardlink: hardlink -~~~~ +~~~~~~~~ Either ``yes`` or ``no``, indicating whether to use hard links instead of moving or copying or symlinking files. (It conflicts with the ``move``, diff --git a/test/test_files.py b/test/test_files.py index 38689bbd9..834d3391c 100644 --- a/test/test_files.py +++ b/test/test_files.py @@ -148,8 +148,8 @@ class MoveTest(_common.TestCase): s1 = os.stat(self.path) s2 = os.stat(self.dest) self.assertTrue( - (s1[stat.ST_INO], s1[stat.ST_DEV]) == \ - (s2[stat.ST_INO], s2[stat.ST_DEV]) + (s1[stat.ST_INO], s1[stat.ST_DEV]) == + (s2[stat.ST_INO], s2[stat.ST_DEV]) ) @unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks") diff --git a/test/test_importer.py b/test/test_importer.py index c372d04da..26dec3de8 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -370,8 +370,8 @@ class NonAutotaggedImportTest(_common.TestCase, ImportHelper): s1 = os.stat(mediafile.path) s2 = os.stat(filename) self.assertTrue( - (s1[stat.ST_INO], s1[stat.ST_DEV]) == \ - (s2[stat.ST_INO], s2[stat.ST_DEV]) + (s1[stat.ST_INO], s1[stat.ST_DEV]) == + (s2[stat.ST_INO], s2[stat.ST_DEV]) ) diff --git a/test/test_mediafile_edge.py b/test/test_mediafile_edge.py index c631cdecf..657ca455d 100644 --- a/test/test_mediafile_edge.py +++ b/test/test_mediafile_edge.py @@ -197,6 +197,7 @@ class SafetyTest(unittest.TestCase, _common.TempDirMixin): finally: os.unlink(fn) + class SideEffectsTest(unittest.TestCase): def setUp(self): self.empty = os.path.join(_common.RSRC, b'empty.mp3') From 4e77ef44844810e0749f86fd0b6c6c8e7ceb5178 Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Sun, 19 Feb 2017 20:48:34 -0600 Subject: [PATCH 137/193] Address review comments --- beets/util/__init__.py | 22 ++++++++++------------ docs/reference/config.rst | 8 +++----- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index d7d45941c..f6cd488d6 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -478,16 +478,15 @@ def move(path, dest, replace=False): def link(path, dest, replace=False): """Create a symbolic link from path to `dest`. Raises an OSError if `dest` already exists, unless `replace` is True. Does nothing if - `path` == `dest`.""" - if (samefile(path, dest)): + `path` == `dest`. + """ + if samefile(path, dest): return - path = syspath(path) - dest = syspath(dest) - if os.path.exists(dest) and not replace: + if os.path.exists(syspath(dest)) and not replace: raise FilesystemError(u'file exists', 'rename', (path, dest)) try: - os.symlink(path, dest) + os.symlink(syspath(path), syspath(dest)) except NotImplementedError: # raised on python >= 3.2 and Windows versions before Vista raise FilesystemError(u'OS does not support symbolic links.' @@ -504,16 +503,15 @@ def link(path, dest, replace=False): def hardlink(path, dest, replace=False): """Create a hard link from path to `dest`. Raises an OSError if `dest` already exists, unless `replace` is True. Does nothing if - `path` == `dest`.""" - if (samefile(path, dest)): + `path` == `dest`. + """ + if samefile(path, dest): return - path = syspath(path) - dest = syspath(dest) - if os.path.exists(dest) and not replace: + if os.path.exists(syspath(dest)) and not replace: raise FilesystemError(u'file exists', 'rename', (path, dest)) try: - os.link(path, dest) + os.link(syspath(path), syspath(dest)) except NotImplementedError: raise FilesystemError(u'OS does not support hard links.' 'link', (path, dest), traceback.format_exc()) diff --git a/docs/reference/config.rst b/docs/reference/config.rst index bf3d89b2e..40f8e9185 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -451,11 +451,9 @@ Either ``yes`` or ``no``, indicating whether to use hard links instead of moving or copying or symlinking files. (It conflicts with the ``move``, ``copy``, and ``link`` options.) Defaults to ``no``. -This option only works on platforms that support hardlinks: i.e., Unixes. -It will fail on Windows. - -It's likely that you'll also want to set ``write`` to ``no`` if you use this -option to preserve the metadata on the linked files. +As with symbolic links (see :ref:`link`, above), this will not work on Windows +and you will want to set ``write`` to ``no``. Otherwise meatadata on the +original file will be modified. resume ~~~~~~ From 26408dd58da631673c23c2ffd2e5dbf660bdae1f Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 19 Feb 2017 22:44:28 -0500 Subject: [PATCH 138/193] Add a little more logging about matching process This could help debug #2446. It will help, at least, narrow down when the Munkres algorithm is taking too long. --- beets/autotag/match.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 71d80e821..71b62adb7 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -103,7 +103,9 @@ def assign_items(items, tracks): costs.append(row) # Find a minimum-cost bipartite matching. + log.debug('Computing track assignment...') matching = Munkres().compute(costs) + log.debug('...done.') # Produce the output matching. mapping = dict((items[i], tracks[j]) for (i, j) in matching) @@ -349,7 +351,8 @@ def _add_candidate(items, results, info): checking the track count, ordering the items, checking for duplicates, and calculating the distance. """ - log.debug(u'Candidate: {0} - {1}', info.artist, info.album) + log.debug(u'Candidate: {0} - {1} ({2})', + info.artist, info.album, info.album_id) # Discard albums with zero tracks. if not info.tracks: From 8ccdb1c77e2becc851e2996b182eecedf40c16ba Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 19 Feb 2017 22:54:14 -0500 Subject: [PATCH 139/193] More logging for MusicBrainz requests Even more performance-isolating logging to help debug #2446. --- beets/autotag/mb.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index a0d4dc33a..a6133adb1 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -374,6 +374,7 @@ def match_album(artist, album, tracks=None): return try: + log.debug(u'Searching for MusicBrainz releases with: {!r}', criteria) res = musicbrainzngs.search_releases( limit=config['musicbrainz']['searchlimit'].get(int), **criteria) except musicbrainzngs.MusicBrainzError as exc: @@ -424,6 +425,7 @@ def album_for_id(releaseid): object or None if the album is not found. May raise a MusicBrainzAPIError. """ + log.debug(u'Requesting MusicBrainz release {}', releaseid) albumid = _parse_id(releaseid) if not albumid: log.debug(u'Invalid MBID ({0}).', releaseid) From d10bfa1af6309e50a7e0954cba21e6491206bcf0 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 20 Feb 2017 22:11:34 -0500 Subject: [PATCH 140/193] Changelog & thanks for #2445 --- docs/changelog.rst | 3 +++ docs/reference/config.rst | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0774722c8..4734ba05a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,6 +28,9 @@ New features: * :doc:`/plugins/badfiles`: Added a ``--verbose`` or ``-v`` option. Results are now displayed only for corrupted files by default and for all the files when the verbose option is set. :bug:`1654` :bug:`2434` +* A new :ref:`hardlink` config option instructs the importer to create hard + links on filesystems that support them. Thanks to :user:`jacobwgillespie`. + :bug:`2445` Fixes: diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 40f8e9185..094462d2f 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -452,7 +452,7 @@ moving or copying or symlinking files. (It conflicts with the ``move``, ``copy``, and ``link`` options.) Defaults to ``no``. As with symbolic links (see :ref:`link`, above), this will not work on Windows -and you will want to set ``write`` to ``no``. Otherwise meatadata on the +and you will want to set ``write`` to ``no``. Otherwise, metadata on the original file will be modified. resume From 3e4c9b8c06fb78477aa95e1056f44b9c17ed7d0b Mon Sep 17 00:00:00 2001 From: "Robin H. Johnson" Date: Mon, 20 Feb 2017 16:24:17 -0800 Subject: [PATCH 141/193] discogs: support simple auth. The official OAuth authentication seems to have broken, so allow usage of simple configuration instead. See-Also: https://github.com/discogs/discogs_client/issues/78 Signed-off-by: Robin H. Johnson --- beetsplug/discogs.py | 8 ++++++++ docs/plugins/discogs.rst | 22 ++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 3b3f5f201..c9e4f3c18 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -54,9 +54,11 @@ class DiscogsPlugin(BeetsPlugin): 'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy', 'tokenfile': 'discogs_token.json', 'source_weight': 0.5, + 'user_token': None, }) self.config['apikey'].redact = True self.config['apisecret'].redact = True + self.config['user_token'].redact = True self.discogs_client = None self.register_listener('import_begin', self.setup) @@ -66,6 +68,12 @@ class DiscogsPlugin(BeetsPlugin): c_key = self.config['apikey'].as_str() c_secret = self.config['apisecret'].as_str() + user_token = self.config['user_token'].as_str() + + if user_token is not None and user_token != '': + self.discogs_client = Client(USER_AGENT, user_token=user_token) + return + # Get the OAuth token from a file or log in. try: with open(self._tokenfile()) as f: diff --git a/docs/plugins/discogs.rst b/docs/plugins/discogs.rst index eb60cca58..56413eed6 100644 --- a/docs/plugins/discogs.rst +++ b/docs/plugins/discogs.rst @@ -14,10 +14,9 @@ To use the ``discogs`` plugin, first enable it in your configuration (see pip install discogs-client -You will also need to register for a `Discogs`_ account. The first time you -run the :ref:`import-cmd` command after enabling the plugin, it will ask you -to authorize with Discogs by visiting the site in a browser. Subsequent runs -will not require re-authorization. +You will also need to register for a `Discogs`_ account, and provide +authentication credentials via a personal access token or an OAuth2 +authorization. Matches from Discogs will now show up during import alongside matches from MusicBrainz. @@ -25,6 +24,21 @@ MusicBrainz. If you have a Discogs ID for an album you want to tag, you can also enter it at the "enter Id" prompt in the importer. +OAuth authorization +``````````````````` +The first time you run the :ref:`import-cmd` command after enabling the plugin, +it will ask you to authorize with Discogs by visiting the site in a browser. +Subsequent runs will not require re-authorization. + +Authentication via personal access token +```````````````````````````````````````` +To get a personal access token (called a user token in the `discogs-client`_ +documentation), login to `Discogs`_, and visit the +`Developer settings page +`_. Press the ``Generate new +token`` button, and place the generated token in your configuration, as the +``user_token`` config option in the ``discogs`` section. + Troubleshooting --------------- From a29b29f533d3efb71f3df56fd2bb406679b8712d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 21 Feb 2017 09:49:22 -0500 Subject: [PATCH 142/193] Docs improvements for #2447 --- beetsplug/discogs.py | 4 ++-- docs/plugins/discogs.rst | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index c9e4f3c18..a9b708619 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -68,9 +68,9 @@ class DiscogsPlugin(BeetsPlugin): c_key = self.config['apikey'].as_str() c_secret = self.config['apisecret'].as_str() + # Try using a configured user token (bypassing OAuth login). user_token = self.config['user_token'].as_str() - - if user_token is not None and user_token != '': + if user_token: self.discogs_client = Client(USER_AGENT, user_token=user_token) return diff --git a/docs/plugins/discogs.rst b/docs/plugins/discogs.rst index 56413eed6..a02b34590 100644 --- a/docs/plugins/discogs.rst +++ b/docs/plugins/discogs.rst @@ -24,15 +24,19 @@ MusicBrainz. If you have a Discogs ID for an album you want to tag, you can also enter it at the "enter Id" prompt in the importer. -OAuth authorization +OAuth Authorization ``````````````````` + The first time you run the :ref:`import-cmd` command after enabling the plugin, it will ask you to authorize with Discogs by visiting the site in a browser. Subsequent runs will not require re-authorization. -Authentication via personal access token +Authentication via Personal Access Token ```````````````````````````````````````` -To get a personal access token (called a user token in the `discogs-client`_ + +As an alternative to OAuth, you can get a token from Discogs and add it to +your configuration. +To get a personal access token (called a "user token" in the `discogs-client`_ documentation), login to `Discogs`_, and visit the `Developer settings page `_. Press the ``Generate new From 6cb9504403f5acba07d1614f037cbd37e65735f5 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 21 Feb 2017 09:49:56 -0500 Subject: [PATCH 143/193] Changelog for #2447 --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4734ba05a..2fbb453c7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -48,6 +48,8 @@ Fixes: command is not found or exists with an error. :bug:`2430` :bug:`2433` * :doc:`/plugins/lyrics`: The Google search backend no longer crashes when the server responds with an error. :bug:`2437` +* :doc:`/plugins/discogs`: You can now authenticate with Discogs using a + personal access token. :bug:`2447` 1.4.3 (January 9, 2017) From d6905fa4c0ba3fcf8ddfbc1d8cc1c863c4c6dc62 Mon Sep 17 00:00:00 2001 From: Stephane Fontaine Date: Tue, 21 Feb 2017 21:29:52 +0400 Subject: [PATCH 144/193] Decode bytes to str before call to path_test Fixes #2443 --- beets/importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/importer.py b/beets/importer.py index bbe152cd4..690a499ff 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -988,7 +988,7 @@ class ArchiveImportTask(SentinelImportTask): `toppath` to that directory. """ for path_test, handler_class in self.handlers(): - if path_test(self.toppath): + if path_test(util.py3_path(self.toppath)): break try: From 74c0e0d6e9075df94361a4588d4c4271e062e88f Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 21 Feb 2017 13:56:20 -0500 Subject: [PATCH 145/193] Fix default for user_token Always match the expected type. --- beetsplug/discogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index a9b708619..0957b3403 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -54,7 +54,7 @@ class DiscogsPlugin(BeetsPlugin): 'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy', 'tokenfile': 'discogs_token.json', 'source_weight': 0.5, - 'user_token': None, + 'user_token': '', }) self.config['apikey'].redact = True self.config['apisecret'].redact = True From 8bfdabb0c60acbd379f20b6682b114943233bf17 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 21 Feb 2017 21:43:13 -0500 Subject: [PATCH 146/193] Changelog entry for #2448 --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2fbb453c7..c3f950d2d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -50,6 +50,8 @@ Fixes: server responds with an error. :bug:`2437` * :doc:`/plugins/discogs`: You can now authenticate with Discogs using a personal access token. :bug:`2447` +* Fix Python 3 compatibility when extracting rar archives in the importer. + Thanks to :user:`Lompik`. :bug:`2443` :bug:`2448` 1.4.3 (January 9, 2017) From 77a6b0edf8d38a3600b27c6754915dc64e56b8d7 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 21 Feb 2017 22:00:31 -0500 Subject: [PATCH 147/193] duplicates: Fix 2nd bug in #2444 about path types --- beetsplug/duplicates.py | 7 ++++--- docs/changelog.rst | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 5ca314d3f..93d53c58a 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -21,7 +21,8 @@ import shlex from beets.plugins import BeetsPlugin from beets.ui import decargs, print_, Subcommand, UserError -from beets.util import command_output, displayable_path, subprocess +from beets.util import command_output, displayable_path, subprocess, \ + bytestring_path from beets.library import Item, Album import six @@ -112,14 +113,14 @@ class DuplicatesPlugin(BeetsPlugin): self.config.set_args(opts) album = self.config['album'].get(bool) checksum = self.config['checksum'].get(str) - copy = self.config['copy'].get(str) + copy = bytestring_path(self.config['copy'].as_str()) count = self.config['count'].get(bool) delete = self.config['delete'].get(bool) fmt = self.config['format'].get(str) full = self.config['full'].get(bool) keys = self.config['keys'].as_str_seq() merge = self.config['merge'].get(bool) - move = self.config['move'].get(str) + move = bytestring_path(self.config['move'].as_str()) path = self.config['path'].get(bool) tiebreak = self.config['tiebreak'].get(dict) strict = self.config['strict'].get(bool) diff --git a/docs/changelog.rst b/docs/changelog.rst index c3f950d2d..f7315e8c1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -52,6 +52,8 @@ Fixes: personal access token. :bug:`2447` * Fix Python 3 compatibility when extracting rar archives in the importer. Thanks to :user:`Lompik`. :bug:`2443` :bug:`2448` +* :doc:`/plugins/duplicates`: Fix Python 3 compatibility when using the + ``copy`` and ``move`` options. :bug:`2444` 1.4.3 (January 9, 2017) From b1ee9716bfb11a2999e4280715c6d4a0abdf7d71 Mon Sep 17 00:00:00 2001 From: rachmadaniHaryono Date: Fri, 24 Feb 2017 10:18:56 +0800 Subject: [PATCH 148/193] chg: dev: add main module. --- beets/__main__.py | 6 ++++++ beets/ui/__init__.py | 10 ++++++++++ 2 files changed, 16 insertions(+) create mode 100644 beets/__main__.py diff --git a/beets/__main__.py b/beets/__main__.py new file mode 100644 index 000000000..8cb407ae6 --- /dev/null +++ b/beets/__main__.py @@ -0,0 +1,6 @@ +"""main module.""" +import sys +from .ui import main + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index ae30a9c60..907687588 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -946,6 +946,16 @@ class SubcommandsOptionParser(CommonOptionsParser): self.subcommands = [] + def get_prog_name(self): + """Get program name. + + Returns: + Program name. + """ + prog_name = super(SubcommandsOptionParser, self).get_prog_name() + prog_name = 'beets' if prog_name == '__main__.py' else prog_name + return prog_name + def add_subcommand(self, *cmds): """Adds a Subcommand object to the parser's list of commands. """ From 87311d69f19c2fb391d7d6ab7085753d8afb101d Mon Sep 17 00:00:00 2001 From: rachmadaniHaryono Date: Fri, 24 Feb 2017 13:47:22 +0800 Subject: [PATCH 149/193] fix: dev: fix c101 from flake8. --- beets/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beets/__main__.py b/beets/__main__.py index 8cb407ae6..7b5fc798f 100644 --- a/beets/__main__.py +++ b/beets/__main__.py @@ -1,4 +1,5 @@ """main module.""" +# -*- coding: utf-8 -*- import sys from .ui import main From 2dff0254190e0f6d9361b25b96d7c5662b1d4f66 Mon Sep 17 00:00:00 2001 From: rachmadaniHaryono Date: Sat, 25 Feb 2017 03:46:26 +0800 Subject: [PATCH 150/193] chg: dev: let program decide the name. --- beets/ui/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 907687588..ae30a9c60 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -946,16 +946,6 @@ class SubcommandsOptionParser(CommonOptionsParser): self.subcommands = [] - def get_prog_name(self): - """Get program name. - - Returns: - Program name. - """ - prog_name = super(SubcommandsOptionParser, self).get_prog_name() - prog_name = 'beets' if prog_name == '__main__.py' else prog_name - return prog_name - def add_subcommand(self, *cmds): """Adds a Subcommand object to the parser's list of commands. """ From 8fa50a41cda9dd8a460182d262090059a7de1665 Mon Sep 17 00:00:00 2001 From: rachmadaniHaryono Date: Sat, 25 Feb 2017 12:13:47 +0800 Subject: [PATCH 151/193] chg: dev: add doc and license. --- beets/__main__.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/beets/__main__.py b/beets/__main__.py index 7b5fc798f..5f10c5b9b 100644 --- a/beets/__main__.py +++ b/beets/__main__.py @@ -1,5 +1,31 @@ -"""main module.""" # -*- coding: utf-8 -*- +"""main module. + +This module will be executed when beets module is run with `-m`. + +Example : `python -m beets` + +Related links about __main__.py: + +* python3 docs entry: https://docs.python.org/3/library/__main__.html +* related SO: http://stackoverflow.com/q/4042905 +""" +# This file is part of beets. +# Copyright 2017, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +from __future__ import division, absolute_import, print_function + import sys from .ui import main From 0fe2279129787ac5a369b0b6e402f0283e915fe2 Mon Sep 17 00:00:00 2001 From: rachmadaniHaryono Date: Sat, 25 Feb 2017 12:14:53 +0800 Subject: [PATCH 152/193] chg: docs: add changelog entry. --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index f7315e8c1..f9051dd73 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,6 +31,7 @@ New features: * A new :ref:`hardlink` config option instructs the importer to create hard links on filesystems that support them. Thanks to :user:`jacobwgillespie`. :bug:`2445` +* Added support to run program from module, e.g. `python -m beets`. Fixes: From 35e2ecf3dd64f2fc8a7fe7455e4b2207434d0994 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 25 Feb 2017 13:37:44 -0500 Subject: [PATCH 153/193] Fix order of header comment vs. docstring (#2453) --- beets/__main__.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/beets/__main__.py b/beets/__main__.py index 5f10c5b9b..608cc0bd0 100644 --- a/beets/__main__.py +++ b/beets/__main__.py @@ -1,15 +1,4 @@ # -*- coding: utf-8 -*- -"""main module. - -This module will be executed when beets module is run with `-m`. - -Example : `python -m beets` - -Related links about __main__.py: - -* python3 docs entry: https://docs.python.org/3/library/__main__.html -* related SO: http://stackoverflow.com/q/4042905 -""" # This file is part of beets. # Copyright 2017, Adrian Sampson. # @@ -24,6 +13,18 @@ Related links about __main__.py: # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. +"""main module. + +This module will be executed when beets module is run with `-m`. + +Example : `python -m beets` + +Related links about __main__.py: + +* python3 docs entry: https://docs.python.org/3/library/__main__.html +* related SO: http://stackoverflow.com/q/4042905 +""" + from __future__ import division, absolute_import, print_function import sys From a8d47f2c309c2fe97a162234a371308e332a2c92 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 25 Feb 2017 13:39:14 -0500 Subject: [PATCH 154/193] Simpler __main__ docstring (#2453) --- beets/__main__.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/beets/__main__.py b/beets/__main__.py index 608cc0bd0..8010ca0dd 100644 --- a/beets/__main__.py +++ b/beets/__main__.py @@ -13,16 +13,8 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""main module. - -This module will be executed when beets module is run with `-m`. - -Example : `python -m beets` - -Related links about __main__.py: - -* python3 docs entry: https://docs.python.org/3/library/__main__.html -* related SO: http://stackoverflow.com/q/4042905 +"""The __main__ module lets you run the beets CLI interface by typing +`python -m beets`. """ from __future__ import division, absolute_import, print_function From 1ee53d4e76a55914ddf59ddeb3fb48dd3241afee Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 25 Feb 2017 13:40:15 -0500 Subject: [PATCH 155/193] Fix up changelog for #2453 --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f9051dd73..a0ae8d43b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,7 +31,7 @@ New features: * A new :ref:`hardlink` config option instructs the importer to create hard links on filesystems that support them. Thanks to :user:`jacobwgillespie`. :bug:`2445` -* Added support to run program from module, e.g. `python -m beets`. +* You can now run beets by typing `python -m beets`. :bug:`2453` Fixes: From 1305c9407f7ad7beaf6d72a46756cf556ea838ec Mon Sep 17 00:00:00 2001 From: Teh Awesomer Date: Sat, 25 Feb 2017 12:01:34 -0800 Subject: [PATCH 156/193] mbsubmit plugin : numeric sort in print_tracks (for >=10 track releases) --- beetsplug/mbsubmit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/mbsubmit.py b/beetsplug/mbsubmit.py index 58b357dd3..02bd5f697 100644 --- a/beetsplug/mbsubmit.py +++ b/beetsplug/mbsubmit.py @@ -56,5 +56,5 @@ class MBSubmitPlugin(BeetsPlugin): return [PromptChoice(u'p', u'Print tracks', self.print_tracks)] def print_tracks(self, session, task): - for i in task.items: + for i in sorted(task.items, key=lambda i: i.track): print_data(None, i, self.config['format'].as_str()) From 1c9d42da95e09b67dc496a328d81ba79e179f84d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 25 Feb 2017 15:30:36 -0500 Subject: [PATCH 157/193] Changelog/thanks for #2457 --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index a0ae8d43b..60d60bc40 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -55,6 +55,8 @@ Fixes: Thanks to :user:`Lompik`. :bug:`2443` :bug:`2448` * :doc:`/plugins/duplicates`: Fix Python 3 compatibility when using the ``copy`` and ``move`` options. :bug:`2444` +* :doc:`/plugins/mbsubmit`: The tracks are now sorted. Thanks to + :user:`awesomer`. :bug:`2457` 1.4.3 (January 9, 2017) From d7a12dbe1cb2722496e6a7796a07100ef1b97804 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 27 Feb 2017 13:23:07 -0500 Subject: [PATCH 158/193] Update recommended Python version to 3.6 On the topic of #2460, which points this out for the Windows beets.reg extra. --- docs/guides/main.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index 7feacb6bf..e2e1665da 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -82,7 +82,7 @@ into this if you've installed Python yourself with `Homebrew`_ or otherwise.) If this happens, you can install beets for the current user only (sans ``sudo``) by typing ``pip install --user beets``. If you do that, you might want -to add ``~/Library/Python/2.7/bin`` to your ``$PATH``. +to add ``~/Library/Python/3.6/bin`` to your ``$PATH``. .. _System Integrity Protection: https://support.apple.com/en-us/HT204899 .. _Homebrew: http://brew.sh @@ -93,7 +93,7 @@ Installing on Windows Installing beets on Windows can be tricky. Following these steps might help you get it right: -1. If you don't have it, `install Python`_ (you want Python 2.7). +1. If you don't have it, `install Python`_ (you want Python 3.6). 2. If you haven't done so already, set your ``PATH`` environment variable to include Python and its scripts. To do so, you have to get the "Properties" From 53ccdad964ea7e1ed19db0e296074bc9fc03004b Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 28 Feb 2017 18:44:10 -0500 Subject: [PATCH 159/193] Update guide for installing Python on Windows As pointed out in #2461. I also notice that the installer now has a checkbox to extend the PATH, so I'm recommending that. --- docs/guides/main.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index e2e1665da..b4e62f472 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -93,14 +93,17 @@ Installing on Windows Installing beets on Windows can be tricky. Following these steps might help you get it right: -1. If you don't have it, `install Python`_ (you want Python 3.6). +1. If you don't have it, `install Python`_ (you want Python 3.6). On the last + screen, the installer gives you the option to "add Python to PATH." Check + this box. If you do that, you can skip the next step. 2. If you haven't done so already, set your ``PATH`` environment variable to include Python and its scripts. To do so, you have to get the "Properties" window for "My Computer", then choose the "Advanced" tab, then hit the "Environment Variables" button, and then look for the ``PATH`` variable in the table. Add the following to the end of the variable's value: - ``;C:\Python27;C:\Python27\Scripts``. + ``;C:\Python36;C:\Python36\Scripts``. You may need to adjust these paths to + point to your Python installation. 3. Next, `install pip`_ (if you don't have it already) by downloading and running the `get-pip.py`_ script. From c420f5917334f937959ba713959e1395e8008873 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 28 Feb 2017 18:46:16 -0500 Subject: [PATCH 160/193] Remove the "install pip" step Python versions 2.7.9+ and 3.4+ come with pip, so we no longer need to instruct people to install it. See #2461. --- docs/guides/main.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index b4e62f472..c90bfc98f 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -105,12 +105,9 @@ get it right: ``;C:\Python36;C:\Python36\Scripts``. You may need to adjust these paths to point to your Python installation. -3. Next, `install pip`_ (if you don't have it already) by downloading and - running the `get-pip.py`_ script. +3. Now install beets by running: ``pip install beets`` -4. Now install beets by running: ``pip install beets`` - -5. You're all set! Type ``beet`` at the command prompt to make sure everything's +4. You're all set! Type ``beet`` at the command prompt to make sure everything's in order. Windows users may also want to install a context menu item for importing files From 55f9b27428961e42a0e195f5d3a9dfbea0b79ed3 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 28 Feb 2017 18:49:52 -0500 Subject: [PATCH 161/193] More admonition to check the beets.reg paths See #2461. --- docs/guides/main.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index c90bfc98f..ec23e8a8f 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -111,10 +111,10 @@ get it right: in order. Windows users may also want to install a context menu item for importing files -into beets. Just download and open `beets.reg`_ to add the necessary keys to the -registry. You can then right-click a directory and choose "Import with beets". -If Python is in a nonstandard location on your system, you may have to edit the -command path manually. +into beets. Download the `beets.reg`_ file and open it in a text file to make +sure the paths to Python match your system. Then double-click the file add the +necessary keys to your registry. You can then right-click a directory and +choose "Import with beets". Because I don't use Windows myself, I may have missed something. If you have trouble or you have more detail to contribute here, please direct it to From f315a17bb21e4c8eb57f8de5fc5f47557d3ad99b Mon Sep 17 00:00:00 2001 From: Pauligrinder Date: Wed, 1 Mar 2017 10:39:40 +0200 Subject: [PATCH 162/193] Added the copyright header Also added config['kodi']['pwd'].redact = True as suggested. --- beetsplug/kodiupdate.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/beetsplug/kodiupdate.py b/beetsplug/kodiupdate.py index 31c978dd6..47ad76d7a 100644 --- a/beetsplug/kodiupdate.py +++ b/beetsplug/kodiupdate.py @@ -1,4 +1,17 @@ # -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2017, Pauli Kettunen. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. """Updates a Kodi library whenever the beets library is changed. This is based on the Plex Update plugin. @@ -39,6 +52,7 @@ class KodiUpdate(BeetsPlugin): u'user': u'kodi', u'pwd': u'kodi'}) + config['kodi']['pwd'].redact = True self.register_listener('database_change', self.listen_for_db_change) def listen_for_db_change(self, lib, model): From 659c17f8257767a8c2b00ff9100d9971a105d692 Mon Sep 17 00:00:00 2001 From: Pauligrinder Date: Wed, 1 Mar 2017 11:11:54 +0200 Subject: [PATCH 163/193] Code fixed according to flake8 --- beetsplug/kodiupdate.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/beetsplug/kodiupdate.py b/beetsplug/kodiupdate.py index 47ad76d7a..78f120d89 100644 --- a/beetsplug/kodiupdate.py +++ b/beetsplug/kodiupdate.py @@ -13,7 +13,8 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Updates a Kodi library whenever the beets library is changed. This is based on the Plex Update plugin. +"""Updates a Kodi library whenever the beets library is changed. +This is based on the Plex Update plugin. Put something like the following in your config.yaml to configure: kodi: @@ -34,13 +35,22 @@ def update_kodi(host, port, user, password): """ url = "http://{0}:{1}/jsonrpc/".format(host, port) - # The kodi jsonrpc documentation states that Content-Type: application/json is mandatory + """Content-Type: application/json is mandatory + according to the kodi jsonrpc documentation""" + headers = {'Content-Type': 'application/json'} + # Create the payload. Id seems to be mandatory. - payload = {'jsonrpc': '2.0', 'method':'AudioLibrary.Scan', 'id':1} - r = requests.post(url, auth=(user, password), json=payload, headers=headers) + payload = {'jsonrpc': '2.0', 'method': 'AudioLibrary.Scan', 'id': 1} + r = requests.post( + url, + auth=(user, password), + json=payload, + headers=headers) + return r + class KodiUpdate(BeetsPlugin): def __init__(self): super(KodiUpdate, self).__init__() @@ -56,7 +66,7 @@ class KodiUpdate(BeetsPlugin): self.register_listener('database_change', self.listen_for_db_change) def listen_for_db_change(self, lib, model): - """Listens for beets db change and register the update for the end""" + """Listens for beets db change and register the update""" self.register_listener('cli_exit', self.update) def update(self, lib): @@ -75,4 +85,3 @@ class KodiUpdate(BeetsPlugin): except requests.exceptions.RequestException: self._log.warning(u'Update failed.') - From 877d4af3443113ef3d33a912a972864828a298d7 Mon Sep 17 00:00:00 2001 From: Pauli Kettunen Date: Wed, 1 Mar 2017 14:55:17 +0200 Subject: [PATCH 164/193] Added documentation for kodiupdate --- docs/plugins/kodiupdate.rst | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/plugins/kodiupdate.rst diff --git a/docs/plugins/kodiupdate.rst b/docs/plugins/kodiupdate.rst new file mode 100644 index 000000000..243e384f2 --- /dev/null +++ b/docs/plugins/kodiupdate.rst @@ -0,0 +1,44 @@ +KodiUpdate Plugin +================= + +``kodiupdate`` is a very simple plugin for beets that lets you automatically +update `Kodi`_'s music library whenever you change your beets library. + +To use ``kodiupdate`` plugin, enable it in your configuration +(see :ref:`using-plugins`). +Then, you'll probably want to configure the specifics of your Kodi host. +You can do that using a ``kodi:`` section in your ``config.yaml``, +which looks like this:: + + kodi: + host: localhost + port: 8080 + user: kodi + pwd: kodi + +To use the ``kodiupdate`` plugin you need to install the `requests`_ library with: + + pip install requests + +You'll also need to enable jsonrpc in Kodi in order the use the plugin. +(System/Settings/Network/Services > Allow control of Kodi via HTTP) + +With that all in place, you'll see beets send the "update" command to your Kodi +host every time you change your beets library. + +.. _Kodi: http://kodi.tv/ +.. _requests: http://docs.python-requests.org/en/latest/ + +Configuration +------------- + +The available options under the ``kodi:`` section are: + +- **host**: The Kodi host name. + Default: ``localhost`` +- **port**: The Kodi host port. + Default: 8080 +- **user**: The Kodi host user. + Default: ``kodi`` +- **pwd**: The Kodi host password. + Default: ``kodi`` From c3c9c50b6ac6967c29773e0c1c2fdfab7f9c47e7 Mon Sep 17 00:00:00 2001 From: Mayuresh Kadu Date: Wed, 1 Mar 2017 13:06:47 +0000 Subject: [PATCH 165/193] Updated main.rst - Updates to seeing your music Modified "Seeing your Music" section to add explanation of the ``-f`` param of the list command. I strongly believe that first time users would very much like to start by overriding the default format of displaying results. --- docs/guides/main.rst | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index e2e1665da..d221ff810 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -230,11 +230,14 @@ songs. Thus:: The Magnetic Fields - Distortion - Three-Way The Magnetic Fields - Distortion - California Girls The Magnetic Fields - Distortion - Old Fools + $ beet ls hissing gronlandic of Montreal - Hissing Fauna, Are You the Destroyer? - Gronlandic Edit + $ beet ls bird The Knife - The Knife - Bird The Mae Shi - Terrorbird - Revelation Six + $ beet ls album:bird The Mae Shi - Terrorbird - Revelation Six @@ -245,16 +248,33 @@ put the field before the term, separated by a : character. So ``album:bird`` only looks for ``bird`` in the "album" field of your songs. (Need to know more? :doc:`/reference/query/` will answer all your questions.) -The ``beet list`` command has another useful option worth mentioning, ``-a``, -which searches for albums instead of songs:: +The ``beet list`` command has couple of other useful options worth mentioning. + +``-a``, which searches for albums instead of songs:: $ beet ls -a forever Bon Iver - For Emma, Forever Ago Freezepop - Freezepop Forever +``-f``, which lets you specify what gets displayed in the results of a search:: + + $ beet ls -a forever -f "[$format] $album ($year) - $artist - $title" + [MP3] For Emma, Forever Ago (2009) - Bon Iver - Flume + [AAC] Freezepop Forever (2011) - Freezepop - Harebrained Scheme + + As you can see, Beets has replaced the fields specified by the ``$`` prefix + (e.g. $format, $year) with values for each item in the results. A full + list of fields available for use can be found by running ``beet fields``. If + you'd like beet to use this format as default (without having to use ``-f``) + simply add it to your config file like this:: + + format_item: "[$format] $album ($year) - $artist - $title" + + And yes, the enclosing double-quotes are necessary. + So handy! -Beets also has a ``stats`` command, just in case you want to see how much music +And finally, Beets also has a ``stats`` command, just in case you want to see how much music you have:: $ beet stats From d22a2570081d5cbee9daccb3060b93e5b83d1390 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 1 Mar 2017 20:22:30 -0500 Subject: [PATCH 166/193] Main guide: clarify meaning of plain keyword query Fixes #2463. --- docs/guides/main.rst | 4 +++- docs/reference/query.rst | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index ec23e8a8f..4b5381a3a 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -238,7 +238,9 @@ songs. Thus:: $ beet ls album:bird The Mae Shi - Terrorbird - Revelation Six -As you can see, search terms by default search all attributes of songs. (They're +By default, a search term will match any of a handful of :ref:`common +attributes ` of songs. +(They're also implicitly joined by ANDs: a track must match *all* criteria in order to match the query.) To narrow a search term to a particular metadata field, just put the field before the term, separated by a : character. So ``album:bird`` diff --git a/docs/reference/query.rst b/docs/reference/query.rst index 2f3366d4c..b4789aa10 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -6,6 +6,8 @@ searches that select tracks and albums from your library. This page explains the query string syntax, which is meant to vaguely resemble the syntax used by Web search engines. +.. _keywordquery: + Keyword ------- From 212ece8b8b24b8c22418ab45fd2576e5a9af65e8 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 2 Mar 2017 10:35:29 -0500 Subject: [PATCH 167/193] Docs: Windows Python installer checkbox location See #2462. --- docs/guides/main.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index 4b5381a3a..e0e06099c 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -93,9 +93,9 @@ Installing on Windows Installing beets on Windows can be tricky. Following these steps might help you get it right: -1. If you don't have it, `install Python`_ (you want Python 3.6). On the last - screen, the installer gives you the option to "add Python to PATH." Check - this box. If you do that, you can skip the next step. +1. If you don't have it, `install Python`_ (you want Python 3.6). The + installer should give you the option to "add Python to PATH." Check this + box. If you do that, you can skip the next step. 2. If you haven't done so already, set your ``PATH`` environment variable to include Python and its scripts. To do so, you have to get the "Properties" From 697c7c0b1ff3e4b468a70c7e687e1e0855722a2e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 2 Mar 2017 10:37:26 -0500 Subject: [PATCH 168/193] Docs: explicitly say what ~ means Again, see #2462. --- docs/guides/main.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index e0e06099c..923f3e806 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -142,8 +142,8 @@ place to start:: Change that first path to a directory where you'd like to keep your music. Then, for ``library``, choose a good place to keep a database file that keeps an index of your music. (The config's format is `YAML`_. You'll want to configure your -text editor to use spaces, not real tabs, for indentation.) - +text editor to use spaces, not real tabs, for indentation. Also, ``~`` means +your home directory in these paths, even on Windows.) The default configuration assumes you want to start a new organized music folder (that ``directory`` above) and that you'll *copy* cleaned-up music into that From a0cbba961f79728a421088b41a0915a600f3c8c3 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 2 Mar 2017 10:45:09 -0500 Subject: [PATCH 169/193] Add the Kodi docs to the TOC tree --- docs/plugins/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 0e851d7da..eaeec02aa 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -66,6 +66,7 @@ like this:: inline ipfs keyfinder + kodiupdate lastgenre lastimport lyrics @@ -148,6 +149,8 @@ Interoperability * :doc:`embyupdate`: Automatically notifies `Emby`_ whenever the beets library changes. * :doc:`importfeeds`: Keep track of imported files via ``.m3u`` playlist file(s) or symlinks. * :doc:`ipfs`: Import libraries from friends and get albums from them via ipfs. +* :doc:`kodiupdate`: Automatically notifies `Kodi`_ whenever the beets library + changes. * :doc:`mpdupdate`: Automatically notifies `MPD`_ whenever the beets library changes. * :doc:`play`: Play beets queries in your music player. @@ -160,6 +163,7 @@ Interoperability .. _Emby: http://emby.media .. _Plex: http://plex.tv +.. _Kodi: http://kodi.tv Miscellaneous ------------- From 791f60b49065dc450e901f62c21a98fb843e7957 Mon Sep 17 00:00:00 2001 From: Mayuresh Kadu Date: Fri, 3 Mar 2017 13:06:09 +0000 Subject: [PATCH 170/193] further updates to Main.rst/ "Seeing your music" Revisions to the text explaining "-f" switch, as discussed in PR #2464 of the parent. --- docs/guides/main.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index d221ff810..fff4c6e82 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -264,13 +264,7 @@ The ``beet list`` command has couple of other useful options worth mentioning. As you can see, Beets has replaced the fields specified by the ``$`` prefix (e.g. $format, $year) with values for each item in the results. A full - list of fields available for use can be found by running ``beet fields``. If - you'd like beet to use this format as default (without having to use ``-f``) - simply add it to your config file like this:: - - format_item: "[$format] $album ($year) - $artist - $title" - - And yes, the enclosing double-quotes are necessary. + list of fields available for use can be found by running ``beet fields``. So handy! From 1fbb557c1c7126c8afac95f500dcc6454853f2ee Mon Sep 17 00:00:00 2001 From: Mayuresh Kadu Date: Fri, 3 Mar 2017 13:11:23 +0000 Subject: [PATCH 171/193] Further updates to main.rst/ "Seeing your music" Un-indented text explaining "-f" switch. --- docs/guides/main.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index fff4c6e82..36ff43ff3 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -262,9 +262,9 @@ The ``beet list`` command has couple of other useful options worth mentioning. [MP3] For Emma, Forever Ago (2009) - Bon Iver - Flume [AAC] Freezepop Forever (2011) - Freezepop - Harebrained Scheme - As you can see, Beets has replaced the fields specified by the ``$`` prefix - (e.g. $format, $year) with values for each item in the results. A full - list of fields available for use can be found by running ``beet fields``. +As you can see, Beets has replaced the fields specified by the ``$`` prefix (e.g. +$format, $year) with values for each item in the results. A full list of fields +available for use can be found by running ``beet fields``. So handy! From 9e755cf7fe64fce6c978ba80f05cd85dbb97165d Mon Sep 17 00:00:00 2001 From: Mayuresh Kadu Date: Fri, 3 Mar 2017 13:14:24 +0000 Subject: [PATCH 172/193] Update main.rst/ Seeing your music Undid adding extra lines to the output. As @sampsyo has rightly pointed out - these would not have appeared in the output. --- docs/guides/main.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index 36ff43ff3..57983e222 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -230,14 +230,11 @@ songs. Thus:: The Magnetic Fields - Distortion - Three-Way The Magnetic Fields - Distortion - California Girls The Magnetic Fields - Distortion - Old Fools - $ beet ls hissing gronlandic of Montreal - Hissing Fauna, Are You the Destroyer? - Gronlandic Edit - $ beet ls bird The Knife - The Knife - Bird The Mae Shi - Terrorbird - Revelation Six - $ beet ls album:bird The Mae Shi - Terrorbird - Revelation Six From 4211050e2cf835272de6ab7e6a8f5d744f888fae Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 3 Mar 2017 10:02:57 -0600 Subject: [PATCH 173/193] Docs refinements for Kodi plugin #2411 --- docs/plugins/kodiupdate.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/plugins/kodiupdate.rst b/docs/plugins/kodiupdate.rst index 243e384f2..a1ec04775 100644 --- a/docs/plugins/kodiupdate.rst +++ b/docs/plugins/kodiupdate.rst @@ -1,12 +1,12 @@ KodiUpdate Plugin ================= -``kodiupdate`` is a very simple plugin for beets that lets you automatically -update `Kodi`_'s music library whenever you change your beets library. +The ``kodiupdate`` plugin lets you automatically update `Kodi`_'s music +library whenever you change your beets library. To use ``kodiupdate`` plugin, enable it in your configuration (see :ref:`using-plugins`). -Then, you'll probably want to configure the specifics of your Kodi host. +Then, you'll want to configure the specifics of your Kodi host. You can do that using a ``kodi:`` section in your ``config.yaml``, which looks like this:: @@ -16,12 +16,12 @@ which looks like this:: user: kodi pwd: kodi -To use the ``kodiupdate`` plugin you need to install the `requests`_ library with: +To use the ``kodiupdate`` plugin you need to install the `requests`_ library with:: pip install requests -You'll also need to enable jsonrpc in Kodi in order the use the plugin. -(System/Settings/Network/Services > Allow control of Kodi via HTTP) +You'll also need to enable JSON-RPC in Kodi in order the use the plugin. +In Kodi's interface, navigate to System/Settings/Network/Services and choose "Allow control of Kodi via HTTP." With that all in place, you'll see beets send the "update" command to your Kodi host every time you change your beets library. From 38d2aeaed9d0bc30ad103efc7b045c5827f7aa9f Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 3 Mar 2017 10:04:46 -0600 Subject: [PATCH 174/193] Changelog for Kodi plugin (#2411) --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 60d60bc40..2ceed486d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -32,6 +32,8 @@ New features: links on filesystems that support them. Thanks to :user:`jacobwgillespie`. :bug:`2445` * You can now run beets by typing `python -m beets`. :bug:`2453` +* A new :doc:`/plugins/kodiupdate` lets you keep your Kodi library in sync + with beets. Thanks to :user:`Pauligrinder`. :bug:`2411` Fixes: From a8c46d8b32724d23b6abcc30730f6d54a2238e35 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 3 Mar 2017 10:17:49 -0600 Subject: [PATCH 175/193] Brevity/clarity improvements for #2464 --- docs/guides/main.rst | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index c230b662a..f0b3635c2 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -247,27 +247,23 @@ put the field before the term, separated by a : character. So ``album:bird`` only looks for ``bird`` in the "album" field of your songs. (Need to know more? :doc:`/reference/query/` will answer all your questions.) -The ``beet list`` command has couple of other useful options worth mentioning. - -``-a``, which searches for albums instead of songs:: +The ``beet list`` command also has an ``-a`` option, which searches for albums instead of songs:: $ beet ls -a forever Bon Iver - For Emma, Forever Ago Freezepop - Freezepop Forever -``-f``, which lets you specify what gets displayed in the results of a search:: +There's also an ``-f`` option (for *format*) that lets you specify what gets displayed in the results of a search:: $ beet ls -a forever -f "[$format] $album ($year) - $artist - $title" [MP3] For Emma, Forever Ago (2009) - Bon Iver - Flume [AAC] Freezepop Forever (2011) - Freezepop - Harebrained Scheme - -As you can see, Beets has replaced the fields specified by the ``$`` prefix (e.g. -$format, $year) with values for each item in the results. A full list of fields -available for use can be found by running ``beet fields``. - -So handy! -And finally, Beets also has a ``stats`` command, just in case you want to see how much music +In the format option, field references like `$format` and `$year` are filled +in with data from each result. You can see a full list of available fields by +running ``beet fields``. + +Beets also has a ``stats`` command, just in case you want to see how much music you have:: $ beet stats From d356356111977c06e4227011aed919ca4ab2df74 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 3 Mar 2017 11:59:50 -0600 Subject: [PATCH 176/193] Fix #2466: GIO returns bytes, so decode them --- beetsplug/thumbnails.py | 8 +++++++- docs/changelog.rst | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/beetsplug/thumbnails.py b/beetsplug/thumbnails.py index 1ea90f01e..838206156 100644 --- a/beetsplug/thumbnails.py +++ b/beetsplug/thumbnails.py @@ -289,4 +289,10 @@ class GioURI(URIGetter): raise finally: self.libgio.g_free(uri_ptr) - return uri + + try: + return uri.decode(util._fsencoding()) + except UnicodeDecodeError: + raise RuntimeError( + "Could not decode filename from GIO: {!r}".format(uri) + ) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2ceed486d..7e86755fb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -59,6 +59,8 @@ Fixes: ``copy`` and ``move`` options. :bug:`2444` * :doc:`/plugins/mbsubmit`: The tracks are now sorted. Thanks to :user:`awesomer`. :bug:`2457` +* :doc:`/plugins/thumbnails`: Fix a string-related crash on Python 3. + :bug:`2466` 1.4.3 (January 9, 2017) From 17ad3e83dbe20126ff0048c29e1c1dd245845645 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 3 Mar 2017 12:10:26 -0600 Subject: [PATCH 177/193] Test updates for #2466 fix --- test/test_thumbnails.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_thumbnails.py b/test/test_thumbnails.py index d287c073a..dc03f06f7 100644 --- a/test/test_thumbnails.py +++ b/test/test_thumbnails.py @@ -276,12 +276,12 @@ class ThumbnailsTest(unittest.TestCase, TestHelper): if not gio.available: self.skipTest(u"GIO library not found") - self.assertEqual(gio.uri(u"/foo"), b"file:///") # silent fail - self.assertEqual(gio.uri(b"/foo"), b"file:///foo") - self.assertEqual(gio.uri(b"/foo!"), b"file:///foo!") + self.assertEqual(gio.uri(u"/foo"), u"file:///") # silent fail + self.assertEqual(gio.uri(b"/foo"), u"file:///foo") + self.assertEqual(gio.uri(b"/foo!"), u"file:///foo!") self.assertEqual( gio.uri(b'/music/\xec\x8b\xb8\xec\x9d\xb4'), - b'file:///music/%EC%8B%B8%EC%9D%B4') + u'file:///music/%EC%8B%B8%EC%9D%B4') def suite(): From 8d14e1b6df03b69f9be008ea7ffed21576f06170 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 19:07:06 -0500 Subject: [PATCH 178/193] smartplaylist: Support overlapping playlist defs See http://discourse.beets.io/t/beets-path-handling-for-delimited-fields/44 --- beetsplug/smartplaylist.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 514e74acc..009512c5c 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -172,6 +172,9 @@ class SmartPlaylistPlugin(BeetsPlugin): if relative_to: relative_to = normpath(relative_to) + # Maps playlist filenames to lists of track filenames. + m3us = {} + for playlist in self._matched_playlists: name, (query, q_sort), (album_query, a_q_sort) = playlist self._log.debug(u"Creating playlist {0}", name) @@ -183,7 +186,6 @@ class SmartPlaylistPlugin(BeetsPlugin): for album in lib.albums(album_query, a_q_sort): items.extend(album.items()) - m3us = {} # As we allow tags in the m3u names, we'll need to iterate through # the items and generate the correct m3u file names. for item in items: @@ -196,13 +198,14 @@ class SmartPlaylistPlugin(BeetsPlugin): item_path = os.path.relpath(item.path, relative_to) if item_path not in m3us[m3u_name]: m3us[m3u_name].append(item_path) - # Now iterate through the m3us that we need to generate - for m3u in m3us: - m3u_path = normpath(os.path.join(playlist_dir, - bytestring_path(m3u))) - mkdirall(m3u_path) - with open(syspath(m3u_path), 'wb') as f: - for path in m3us[m3u]: - f.write(path + b'\n') + + # Write all of the accumulated track lists to files. + for m3u in m3us: + m3u_path = normpath(os.path.join(playlist_dir, + bytestring_path(m3u))) + mkdirall(m3u_path) + with open(syspath(m3u_path), 'wb') as f: + for path in m3us[m3u]: + f.write(path + b'\n') self._log.info(u"{0} playlists updated", len(self._matched_playlists)) From ad4cd7a447e5ff8504481e9e7259ae02c39dfb3b Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 22:58:00 -0500 Subject: [PATCH 179/193] Fix #2469: Beatport track limit It looks like the API uses pagination, and the default page size is 10. An easy fix is to just request lots of tracks per page (here, 100). A nicer thing to do in the future would be to actually traverse multiple pages. --- beetsplug/beatport.py | 3 ++- docs/changelog.rst | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index c62abf7ab..8a73efa16 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -161,7 +161,8 @@ class BeatportClient(object): :returns: Tracks in the matching release :rtype: list of :py:class:`BeatportTrack` """ - response = self._get('/catalog/3/tracks', releaseId=beatport_id) + response = self._get('/catalog/3/tracks', releaseId=beatport_id, + perPage=100) return [BeatportTrack(t) for t in response] def get_track(self, beatport_id): diff --git a/docs/changelog.rst b/docs/changelog.rst index 7e86755fb..9ad8b3b4d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -61,6 +61,8 @@ Fixes: :user:`awesomer`. :bug:`2457` * :doc:`/plugins/thumbnails`: Fix a string-related crash on Python 3. :bug:`2466` +* :doc:`/plugins/beatport`: More than just 10 songs are now fetched per album. + :bug:`2469` 1.4.3 (January 9, 2017) From 1fbbcf65fde067bd753907dc18218acc3f7dddee Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 23:29:09 -0500 Subject: [PATCH 180/193] Fix some confusing indentation --- beetsplug/embedart.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 6344dc2b9..f7e92e6f1 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -69,26 +69,26 @@ class EmbedCoverArtPlugin(BeetsPlugin): def _confirmation(items, opts): # Confirm artwork changes to library items. - if not opts.yes: - # Prepare confirmation with user. - print_() + if not opts.yes: + # Prepare confirmation with user. + print_() - fmt = u'$albumartist - $album' - istr = u'album' - if opts.file: - fmt = u'$albumartist - $album - $title' - istr = u'file' - prompt = u'Modify artwork for %i %s%s (y/n)?' % \ - (len(items), istr, 's' if len(items) > 1 else '') + fmt = u'$albumartist - $album' + istr = u'album' + if opts.file: + fmt = u'$albumartist - $album - $title' + istr = u'file' + prompt = u'Modify artwork for %i %s%s (y/n)?' % \ + (len(items), istr, 's' if len(items) > 1 else '') - # Show all the items. - for item in items: - print_(format(item, fmt)) + # Show all the items. + for item in items: + print_(format(item, fmt)) - # Confirm with user. - if not ui.input_yn(prompt, True): - return False - return True + # Confirm with user. + if not ui.input_yn(prompt, True): + return False + return True def embed_func(lib, opts, args): if opts.file: From c8499eb56d55b1f7d24ea26fbcba681ca427b92e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 23:34:53 -0500 Subject: [PATCH 181/193] _confirmation does not need to be a closure --- beetsplug/embedart.py | 50 +++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index f7e92e6f1..658b99599 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -27,6 +27,33 @@ from beets import config from beets import art +def _confirmation(items, opts): + """Show the list of affected objects (items or albums) and confirm + that the user wants to modify their artwork. + """ + # Confirm artwork changes to library items. + if not opts.yes: + # Prepare confirmation with user. + print_() + + fmt = u'$albumartist - $album' + istr = u'album' + if opts.file: + fmt = u'$albumartist - $album - $title' + istr = u'file' + prompt = u'Modify artwork for %i %s%s (y/n)?' % \ + (len(items), istr, 's' if len(items) > 1 else '') + + # Show all the items. + for item in items: + print_(format(item, fmt)) + + # Confirm with user. + if not ui.input_yn(prompt, True): + return False + return True + + class EmbedCoverArtPlugin(BeetsPlugin): """Allows albumart to be embedded into the actual files. """ @@ -67,29 +94,6 @@ class EmbedCoverArtPlugin(BeetsPlugin): compare_threshold = self.config['compare_threshold'].get(int) ifempty = self.config['ifempty'].get(bool) - def _confirmation(items, opts): - # Confirm artwork changes to library items. - if not opts.yes: - # Prepare confirmation with user. - print_() - - fmt = u'$albumartist - $album' - istr = u'album' - if opts.file: - fmt = u'$albumartist - $album - $title' - istr = u'file' - prompt = u'Modify artwork for %i %s%s (y/n)?' % \ - (len(items), istr, 's' if len(items) > 1 else '') - - # Show all the items. - for item in items: - print_(format(item, fmt)) - - # Confirm with user. - if not ui.input_yn(prompt, True): - return False - return True - def embed_func(lib, opts, args): if opts.file: imagepath = normpath(opts.file) From 430595825f624e00cf7cb4a86bd8568fd02257a3 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 23:37:04 -0500 Subject: [PATCH 182/193] Rename parameter to be more sensible We don't just take items; they're either items or albums. --- beetsplug/embedart.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 658b99599..c3463c1a6 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -27,7 +27,7 @@ from beets import config from beets import art -def _confirmation(items, opts): +def _confirmation(objs, opts): """Show the list of affected objects (items or albums) and confirm that the user wants to modify their artwork. """ @@ -42,11 +42,11 @@ def _confirmation(items, opts): fmt = u'$albumartist - $album - $title' istr = u'file' prompt = u'Modify artwork for %i %s%s (y/n)?' % \ - (len(items), istr, 's' if len(items) > 1 else '') + (len(objs), istr, 's' if len(objs) > 1 else '') - # Show all the items. - for item in items: - print_(format(item, fmt)) + # Show all the items or albums. + for obj in objs: + print_(format(obj, fmt)) # Confirm with user. if not ui.input_yn(prompt, True): From eddbab63bd5e4efaaeeab42220771a5e9387b1fe Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 23:37:33 -0500 Subject: [PATCH 183/193] Remove extraneous blank line --- beetsplug/embedart.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index c3463c1a6..39cab585e 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -33,9 +33,6 @@ def _confirmation(objs, opts): """ # Confirm artwork changes to library items. if not opts.yes: - # Prepare confirmation with user. - print_() - fmt = u'$albumartist - $album' istr = u'album' if opts.file: From 3a436bb33dc5553c3d447a5d77a650a5b248466a Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 23:38:16 -0500 Subject: [PATCH 184/193] Remove explicit format We now use the default formats from the configuration. --- beetsplug/embedart.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 39cab585e..618761e90 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -33,17 +33,15 @@ def _confirmation(objs, opts): """ # Confirm artwork changes to library items. if not opts.yes: - fmt = u'$albumartist - $album' istr = u'album' if opts.file: - fmt = u'$albumartist - $album - $title' istr = u'file' prompt = u'Modify artwork for %i %s%s (y/n)?' % \ (len(objs), istr, 's' if len(objs) > 1 else '') # Show all the items or albums. for obj in objs: - print_(format(obj, fmt)) + print_(format(obj)) # Confirm with user. if not ui.input_yn(prompt, True): From f985970b252346f40dc22fdb6ef678442b966eb0 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 23:42:20 -0500 Subject: [PATCH 185/193] Simplify scope of _confirmation and formatting --- beetsplug/embedart.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 618761e90..4f6cf1580 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -27,26 +27,26 @@ from beets import config from beets import art -def _confirmation(objs, opts): +def _confirmation(objs, album): """Show the list of affected objects (items or albums) and confirm that the user wants to modify their artwork. + + `album` is a Boolean indicating whether these are albums (as opposed + to items). """ - # Confirm artwork changes to library items. - if not opts.yes: - istr = u'album' - if opts.file: - istr = u'file' - prompt = u'Modify artwork for %i %s%s (y/n)?' % \ - (len(objs), istr, 's' if len(objs) > 1 else '') + noun = u'album' if album else u'file' + prompt = u'Modify artwork for {} {}{} (y/n)?'.format( + len(objs), + noun, + u's' if len(objs) > 1 else u'' + ) - # Show all the items or albums. - for obj in objs: - print_(format(obj)) + # Show all the items or albums. + for obj in objs: + print_(format(obj)) - # Confirm with user. - if not ui.input_yn(prompt, True): - return False - return True + # Confirm with user. + return ui.input_yn(prompt, True) class EmbedCoverArtPlugin(BeetsPlugin): @@ -100,7 +100,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): items = lib.items(decargs(args)) # Confirm with user. - if not _confirmation(items, opts): + if not opts.yes and not _confirmation(items, not opts.file): return for item in items: @@ -110,7 +110,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): items = lib.albums(decargs(args)) # Confirm with user. - if not _confirmation(items, opts): + if not opts.yes and not _confirmation(items, not opts.file): return for album in items: From f0f55d11ec535ad3159336d19cdd51c937759ca9 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 23:42:38 -0500 Subject: [PATCH 186/193] Rename _confirmation to _confirm --- beetsplug/embedart.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 4f6cf1580..3d7f4f993 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -27,7 +27,7 @@ from beets import config from beets import art -def _confirmation(objs, album): +def _confirm(objs, album): """Show the list of affected objects (items or albums) and confirm that the user wants to modify their artwork. @@ -100,20 +100,20 @@ class EmbedCoverArtPlugin(BeetsPlugin): items = lib.items(decargs(args)) # Confirm with user. - if not opts.yes and not _confirmation(items, not opts.file): + if not opts.yes and not _confirm(items, not opts.file): return for item in items: art.embed_item(self._log, item, imagepath, maxwidth, None, compare_threshold, ifempty) else: - items = lib.albums(decargs(args)) + albums = lib.albums(decargs(args)) # Confirm with user. - if not opts.yes and not _confirmation(items, not opts.file): + if not opts.yes and not _confirm(albums, not opts.file): return - for album in items: + for album in albums: art.embed_album(self._log, album, maxwidth, False, compare_threshold, ifempty) self.remove_artfile(album) From 8ce7b49ed812f4fa704f822c90a6f526dbf2d446 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 23:45:29 -0500 Subject: [PATCH 187/193] Default to confirm --- beetsplug/embedart.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 3d7f4f993..51d401af8 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -35,7 +35,7 @@ def _confirm(objs, album): to items). """ noun = u'album' if album else u'file' - prompt = u'Modify artwork for {} {}{} (y/n)?'.format( + prompt = u'Modify artwork for {} {}{} (Y/n)?'.format( len(objs), noun, u's' if len(objs) > 1 else u'' @@ -46,7 +46,7 @@ def _confirm(objs, album): print_(format(obj)) # Confirm with user. - return ui.input_yn(prompt, True) + return ui.input_yn(prompt) class EmbedCoverArtPlugin(BeetsPlugin): From b91185a9ff7cd7aecd65bbda10e7971921f7b2b3 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 23:53:17 -0500 Subject: [PATCH 188/193] Docs tweaks for #2422 --- docs/changelog.rst | 5 +++-- docs/plugins/embedart.rst | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index fd37f992e..dd597d933 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,8 +31,9 @@ New features: * A new :ref:`hardlink` config option instructs the importer to create hard links on filesystems that support them. Thanks to :user:`jacobwgillespie`. :bug:`2445` -* :doc:`/plugins/embedart` by default now asks for confirmation before - embedding art into music files. Thanks to :user:`Stunner`. :bug:`1999` +* :doc:`/plugins/embedart`: The explicit ``embedart`` command now asks for + confirmation before embedding art into music files. Thanks to + :user:`Stunner`. :bug:`1999` * You can now run beets by typing `python -m beets`. :bug:`2453` * A new :doc:`/plugins/kodiupdate` lets you keep your Kodi library in sync with beets. Thanks to :user:`Pauligrinder`. :bug:`2411` diff --git a/docs/plugins/embedart.rst b/docs/plugins/embedart.rst index 717bfb8e2..68ea0f664 100644 --- a/docs/plugins/embedart.rst +++ b/docs/plugins/embedart.rst @@ -81,8 +81,8 @@ embedded album art: * ``beet embedart [-f IMAGE] QUERY``: embed images into the every track on the albums matching the query. If the ``-f`` (``--file``) option is given, then use a specific image file from the filesystem; otherwise, each album embeds - its own currently associated album art. Will prompt for confirmation before - making the change unless the ``-y`` (``--yes``) option is specified. + its own currently associated album art. The command prompts for confirmation + before making the change unless you specify the ``-y`` (``--yes``) option. * ``beet extractart [-a] [-n FILE] QUERY``: extracts the images for all albums matching the query. The images are placed inside the album folder. You can From 5a71ce722a672431c035ad3b6c430b97fbaacdc9 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 6 Mar 2017 23:55:14 -0500 Subject: [PATCH 189/193] Simplify embedart test changes for #2422 Whenever possible, it's nice to avoid using DummyIO---it can make debugging difficult. --- test/test_embedart.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/test/test_embedart.py b/test/test_embedart.py index b0e66dddb..ad04c57d7 100644 --- a/test/test_embedart.py +++ b/test/test_embedart.py @@ -52,8 +52,6 @@ class EmbedartCliTest(_common.TestCase, TestHelper): def setUp(self): super(EmbedartCliTest, self).setUp() - self.io.install() - self.setup_beets() # Converter is threaded self.load_plugins('embedart') @@ -71,8 +69,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self._setup_data() album = self.add_album_fixture() item = album.items()[0] - self.io.addinput('y') - self.run_command('embedart', '-f', self.small_artpath) + self.run_command('embedart', '-y', '-f', self.small_artpath) mediafile = MediaFile(syspath(item.path)) self.assertEqual(mediafile.images[0].data, self.image_data) @@ -82,8 +79,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): item = album.items()[0] album.artpath = self.small_artpath album.store() - self.io.addinput('y') - self.run_command('embedart') + self.run_command('embedart', '-y') mediafile = MediaFile(syspath(item.path)) self.assertEqual(mediafile.images[0].data, self.image_data) @@ -101,8 +97,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): album.store() config['embedart']['remove_art_file'] = True - self.io.addinput('y') - self.run_command('embedart') + self.run_command('embedart', '-y') if os.path.isfile(tmp_path): os.remove(tmp_path) @@ -112,8 +107,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self.add_album_fixture() logging.getLogger('beets.embedart').setLevel(logging.DEBUG) with self.assertRaises(ui.UserError): - self.io.addinput('y') - self.run_command('embedart', '-f', '/doesnotexist') + self.run_command('embedart', '-y', '-f', '/doesnotexist') def test_embed_non_image_file(self): album = self.add_album_fixture() @@ -124,8 +118,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): os.close(handle) try: - self.io.addinput('y') - self.run_command('embedart', '-f', tmp_path) + self.run_command('embedart', '-y', '-f', tmp_path) finally: os.remove(tmp_path) @@ -137,11 +130,9 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self._setup_data(self.abbey_artpath) album = self.add_album_fixture() item = album.items()[0] - self.io.addinput('y') - self.run_command('embedart', '-f', self.abbey_artpath) + self.run_command('embedart', '-y', '-f', self.abbey_artpath) config['embedart']['compare_threshold'] = 20 - self.io.addinput('y') - self.run_command('embedart', '-f', self.abbey_differentpath) + self.run_command('embedart', '-y', '-f', self.abbey_differentpath) mediafile = MediaFile(syspath(item.path)) self.assertEqual(mediafile.images[0].data, self.image_data, @@ -153,8 +144,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self._setup_data(self.abbey_similarpath) album = self.add_album_fixture() item = album.items()[0] - self.io.addinput('y') - self.run_command('embedart', '-f', self.abbey_artpath) + self.run_command('embedart', '-y', '-f', self.abbey_artpath) config['embedart']['compare_threshold'] = 20 self.run_command('embedart', '-y', '-f', self.abbey_similarpath) mediafile = MediaFile(syspath(item.path)) From 4d90c9645c4901835c8585ad0f8fc38345ae44ec Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 7 Mar 2017 22:55:44 -0500 Subject: [PATCH 190/193] Changelog for #2468 --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index d69012706..f72d22f9a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -34,6 +34,9 @@ New features: * You can now run beets by typing `python -m beets`. :bug:`2453` * A new :doc:`/plugins/kodiupdate` lets you keep your Kodi library in sync with beets. Thanks to :user:`Pauligrinder`. :bug:`2411` +* :doc:`/plugins/smartplaylist`: Different playlist specifications that + generate identically-named playlist files no longer conflict; instead, the + resulting lists of tracks are concatenated. :bug:`2468` Fixes: From f6df3befac07109848594af46459853fd12b51b3 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 8 Mar 2017 19:06:09 -0800 Subject: [PATCH 191/193] Added interactive test method for embedart plugin. --- test/test_embedart.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/test_embedart.py b/test/test_embedart.py index ad04c57d7..30e4841ba 100644 --- a/test/test_embedart.py +++ b/test/test_embedart.py @@ -52,6 +52,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): def setUp(self): super(EmbedartCliTest, self).setUp() + self.io.install() self.setup_beets() # Converter is threaded self.load_plugins('embedart') @@ -65,6 +66,15 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self.unload_plugins() self.teardown_beets() + def test_embed_art_from_file_with_input(self): + self._setup_data() + album = self.add_album_fixture() + item = album.items()[0] + self.io.addinput('y') + self.run_command('embedart', '-f', self.small_artpath) + mediafile = MediaFile(syspath(item.path)) + self.assertEqual(mediafile.images[0].data, self.image_data) + def test_embed_art_from_file(self): self._setup_data() album = self.add_album_fixture() From acbf3b75345e408977c7a7cd54f37bd36fd03254 Mon Sep 17 00:00:00 2001 From: abba23 <628208@gmail.com> Date: Fri, 10 Mar 2017 17:31:23 +0100 Subject: [PATCH 192/193] added beets-popularity plugin --- docs/plugins/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index eaeec02aa..0b2440aa8 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -240,6 +240,8 @@ Here are a few of the plugins written by the beets community: * `beets-usertag`_ lets you use keywords to tag and organize your music. +* `beets-popularity`_ fetches popularity values from Spotify. + .. _beets-check: https://github.com/geigerzaehler/beets-check .. _copyartifacts: https://github.com/sbarakat/beets-copyartifacts .. _dsedivec: https://github.com/dsedivec/beets-plugins @@ -257,3 +259,5 @@ Here are a few of the plugins written by the beets community: .. _beets-noimport: https://gitlab.com/tiago.dias/beets-noimport .. _whatlastgenre: https://github.com/YetAnotherNerd/whatlastgenre/tree/master/plugin/beets .. _beets-usertag: https://github.com/igordertigor/beets-usertag +.. _beets-popularity: https://github.com/abba23/beets-popularity + From 64d69f0817c0282fa71ee980ce3c6d602ce52730 Mon Sep 17 00:00:00 2001 From: Aaron Date: Fri, 10 Mar 2017 23:30:49 -0800 Subject: [PATCH 193/193] =?UTF-8?q?embedart:=20Added=20test=20case=20for?= =?UTF-8?q?=20inputting=20=E2=80=9Cno=E2=80=9D=20option=20interactively.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_embedart.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/test_embedart.py b/test/test_embedart.py index 30e4841ba..1622fffb4 100644 --- a/test/test_embedart.py +++ b/test/test_embedart.py @@ -66,7 +66,7 @@ class EmbedartCliTest(_common.TestCase, TestHelper): self.unload_plugins() self.teardown_beets() - def test_embed_art_from_file_with_input(self): + def test_embed_art_from_file_with_yes_input(self): self._setup_data() album = self.add_album_fixture() item = album.items()[0] @@ -75,6 +75,16 @@ class EmbedartCliTest(_common.TestCase, TestHelper): mediafile = MediaFile(syspath(item.path)) self.assertEqual(mediafile.images[0].data, self.image_data) + def test_embed_art_from_file_with_no_input(self): + self._setup_data() + album = self.add_album_fixture() + item = album.items()[0] + self.io.addinput('n') + self.run_command('embedart', '-f', self.small_artpath) + mediafile = MediaFile(syspath(item.path)) + # make sure that images array is empty (nothing embedded) + self.assertEqual(len(mediafile.images), 0) + def test_embed_art_from_file(self): self._setup_data() album = self.add_album_fixture()