From f684f29a2536dc567eedacac27d843828022cafd Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 21 Mar 2016 10:24:13 -0700 Subject: [PATCH 01/30] lyrics: Tolerate pages without text (fix #1914) --- beetsplug/lyrics.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index b5212b56f..7ed64d88d 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -430,8 +430,13 @@ def scrape_lyrics_from_html(html): parse_only=SoupStrainer(text=is_text_notcode)) except HTMLParseError: return None - soup = sorted(soup.stripped_strings, key=len)[-1] - return soup + + # Get the longest text element (if any). + strings = sorted(soup.stripped_strings, key=len, reverse=True) + if strings: + return strings[0] + else: + return None class Google(Backend): From d1753b341eefe33e925e0e5a3618ede53efef455 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 21 Mar 2016 10:28:30 -0700 Subject: [PATCH 02/30] lyrics: Some comments and better naming --- beetsplug/lyrics.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 7ed64d88d..207b9d084 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -331,8 +331,12 @@ class LyricsWiki(SymbolsReplaced): html = self.fetch_url(url) if not html: return - lyrics = extract_text_in(unescape(html), u"
") - lyrics = scrape_lyrics_from_html(lyrics) + + # Get the HTML fragment inside the appropriate HTML element and then + # extract the text from it. + html_frag = extract_text_in(unescape(html), u"
") + lyrics = scrape_lyrics_from_html(html_frag) + if lyrics and 'Unfortunately, we are not licensed' not in lyrics: return lyrics From 8ef36198fb65b1739c9afdcdc599f5d09cba777b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Tissi=C3=A8res?= Date: Mon, 21 Mar 2016 20:47:05 +0100 Subject: [PATCH 03/30] importadded: preserve mtime after any write --- beetsplug/importadded.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index 1202e95de..71ead7193 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -19,6 +19,7 @@ class ImportAddedPlugin(BeetsPlugin): super(ImportAddedPlugin, self).__init__() self.config.add({ 'preserve_mtimes': False, + 'preserve_write_mtimes': False, }) # item.id for new items that were reimported @@ -37,6 +38,7 @@ class ImportAddedPlugin(BeetsPlugin): register('item_linked', 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) def check_config(self, task, session): self.config['preserve_mtimes'].get(bool) @@ -120,3 +122,13 @@ class ImportAddedPlugin(BeetsPlugin): self._log.debug(u"Import of item '{0}', selected item.added={1}", util.displayable_path(item.path), item.added) item.store() + + def update_after_write_time(self, item): + """Update the mtime of the item's file with the item.added value + after each write of the item if `preserve_write_mtimes` is enabled. + """ + if item.added: + if self.config['preserve_write_mtimes'].get(bool): + self.write_item_mtime(item, item.added) + self._log.debug(u"Write of item '{0}', selected item.added={1}", + util.displayable_path(item.path), item.added) From e5be804802d6a11e9e5e0b8c25ba17999fd2f95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Tissi=C3=A8res?= Date: Tue, 22 Mar 2016 06:24:09 +0100 Subject: [PATCH 04/30] importadded: adapt doc for new option preserve_write_mtimes --- beetsplug/importadded.py | 4 ++-- docs/changelog.rst | 2 ++ docs/plugins/importadded.rst | 15 +++++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index 71ead7193..77c7e7ab8 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -124,8 +124,8 @@ class ImportAddedPlugin(BeetsPlugin): item.store() def update_after_write_time(self, item): - """Update the mtime of the item's file with the item.added value - after each write of the item if `preserve_write_mtimes` is enabled. + """Update the mtime of the item's file with the item.added value + after each write of the item if `preserve_write_mtimes` is enabled. """ if item.added: if self.config['preserve_write_mtimes'].get(bool): diff --git a/docs/changelog.rst b/docs/changelog.rst index 9cc53ac97..f3460cfde 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,8 @@ New features: * :doc:`/plugins/convert`: A new `album_art_maxwidth` lets you resize album art while copying it. +* :doc:`/plugins/importadded`: A new `preserve_write_mtimes` option + lets you preserve mtime of files after each write. Fixes: diff --git a/docs/plugins/importadded.rst b/docs/plugins/importadded.rst index 677879da8..2a2e8ea29 100644 --- a/docs/plugins/importadded.rst +++ b/docs/plugins/importadded.rst @@ -22,8 +22,11 @@ The ``item.added`` field is populated as follows: set to the oldest mtime of the files in the album before they were imported. The mtime of album directories is ignored. -This plugin can optionally be configured to also preserve mtimes using the -``preserve_mtimes`` option. +This plugin can optionally be configured to also preserve mtimes at +import using the ``preserve_mtimes`` option. + +When ``preserve_write_mtimes`` option is set, this plugin preserves +mtimes after each write to files using the ``item.added`` attribute. File modification times are preserved as follows: @@ -40,9 +43,13 @@ Configuration ------------- To configure the plugin, make an ``importadded:`` section in your -configuration file. There is one option available: +configuration file. There are two options available: -- **preserve_mtimes**: After writing files, re-set their mtimes to their +- **preserve_mtimes**: After importing files, re-set their mtimes to their + original value. + Default: ``no``. + +- **preserve_write_mtimes**: After writing files, re-set their mtimes to their original value. Default: ``no``. From 3f4f0772729a2890ecb8d45d5539830bea5f1eb0 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 3 Apr 2016 17:38:40 -0400 Subject: [PATCH 05/30] edit: Log the invoked command --- beetsplug/edit.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/beetsplug/edit.py b/beetsplug/edit.py index 58d0828a5..191091295 100644 --- a/beetsplug/edit.py +++ b/beetsplug/edit.py @@ -40,11 +40,12 @@ class ParseError(Exception): """ -def edit(filename): +def edit(filename, log): """Open `filename` in a text editor. """ cmd = util.shlex_split(util.editor_command()) cmd.append(filename) + log.debug(u'invoking editor command: {!r}', cmd) subprocess.call(cmd) @@ -245,7 +246,7 @@ class EditPlugin(plugins.BeetsPlugin): try: while True: # Ask the user to edit the data. - edit(new.name) + edit(new.name, self._log) # Read the data back after editing and check whether anything # changed. From fa2aa82a0d2142a933ab64adea71de7bc12d1c8f Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 3 Apr 2016 17:42:25 -0400 Subject: [PATCH 06/30] Fix #1927: useful error message for failed edit --- beetsplug/edit.py | 7 ++++++- docs/changelog.rst | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/beetsplug/edit.py b/beetsplug/edit.py index 191091295..5c7796ee8 100644 --- a/beetsplug/edit.py +++ b/beetsplug/edit.py @@ -46,7 +46,12 @@ def edit(filename, log): cmd = util.shlex_split(util.editor_command()) cmd.append(filename) log.debug(u'invoking editor command: {!r}', cmd) - subprocess.call(cmd) + try: + subprocess.call(cmd) + except OSError as exc: + raise ui.UserError(u'could not run editor command {!r}: {}'.format( + cmd[0], exc + )) def dump(arg): diff --git a/docs/changelog.rst b/docs/changelog.rst index f3460cfde..43c2893d7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,8 @@ Fixes: LyricsWiki page markup. :bug:`1912` :bug:`1909` * :doc:`/plugins/lyrics`: Also fix retrieval from Musixmatch and the way we guess the URL for lyrics. :bug:`1880` +* :doc:`/plugins/error`: Fail gracefully when the configured text editor + command can't be invoked. :bug:`1927` 1.3.17 (February 7, 2016) From cb498e07726212143e392ba1e4898b227efa5d1c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 3 Apr 2016 17:50:18 -0400 Subject: [PATCH 07/30] Fix tests for #1927 fix --- test/test_edit.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_edit.py b/test/test_edit.py index 3bb817734..e756f99a2 100644 --- a/test/test_edit.py +++ b/test/test_edit.py @@ -47,7 +47,9 @@ class ModifyFileMocker(object): if replacements: self.action = self.replace_contents - def overwrite_contents(self, filename): + # The two methods below mock the `edit` utility function in the plugin. + + def overwrite_contents(self, filename, log): """Modify `filename`, replacing its contents with `self.contents`. If `self.contents` is empty, the file remains unchanged. """ @@ -55,7 +57,7 @@ class ModifyFileMocker(object): with codecs.open(filename, 'w', encoding='utf8') as f: f.write(self.contents) - def replace_contents(self, filename): + def replace_contents(self, filename, log): """Modify `filename`, reading its contents and replacing the strings specified in `self.replacements`. """ From 1d3637e507965a733fa3bdc78777394b7fd35cf4 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 3 Apr 2016 18:00:44 -0400 Subject: [PATCH 08/30] Fix typo in changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 43c2893d7..67960d56f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,7 +24,7 @@ Fixes: LyricsWiki page markup. :bug:`1912` :bug:`1909` * :doc:`/plugins/lyrics`: Also fix retrieval from Musixmatch and the way we guess the URL for lyrics. :bug:`1880` -* :doc:`/plugins/error`: Fail gracefully when the configured text editor +* :doc:`/plugins/edit`: Fail gracefully when the configured text editor command can't be invoked. :bug:`1927` From d67950cdccfd2362c5bdbe9f1dd7e42ab3d7ef67 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 00:45:55 +0200 Subject: [PATCH 09/30] pep8 --- beetsplug/lyrics.py | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 207b9d084..c766db2a7 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -13,18 +13,15 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Fetches, embeds, and displays lyrics. -""" +from __future__ import absolute_import, division, print_function -from __future__ import division, absolute_import, print_function - -import re -import requests -import json -import unicodedata -import urllib import difflib import itertools +import json +import re +import requests +import unicodedata +import urllib import warnings from HTMLParser import HTMLParseError @@ -56,7 +53,7 @@ URL_CHARACTERS = { def unescape(text): - """Resolves &#xxx; HTML entities (and some others).""" + """Resolve &#xxx; HTML entities (and some others).""" if isinstance(text, bytes): text = text.decode('utf8', 'ignore') out = text.replace(u' ', u' ') @@ -455,29 +452,29 @@ class Google(Backend): """ if not text: return False - badTriggersOcc = [] - nbLines = text.count('\n') - if nbLines <= 1: + bad_triggers_occ = [] + nb_lines = text.count('\n') + if nb_lines <= 1: self._log.debug(u"Ignoring too short lyrics '{0}'", text) return False - elif nbLines < 5: - badTriggersOcc.append('too_short') + elif nb_lines < 5: + bad_triggers_occ.append('too_short') else: # Lyrics look legit, remove credits to avoid being penalized # further down text = remove_credits(text) - badTriggers = ['lyrics', 'copyright', 'property', 'links'] + bad_triggers = ['lyrics', 'copyright', 'property', 'links'] if artist: - badTriggersOcc += [artist] + bad_triggers_occ += [artist] - for item in badTriggers: - badTriggersOcc += [item] * len(re.findall(r'\W%s\W' % item, - text, re.I)) + for item in bad_triggers: + bad_triggers_occ += [item] * len(re.findall(r'\W%s\W' % item, + text, re.I)) - if badTriggersOcc: - self._log.debug(u'Bad triggers detected: {0}', badTriggersOcc) - return len(badTriggersOcc) < 2 + if bad_triggers_occ: + self._log.debug(u'Bad triggers detected: {0}', bad_triggers_occ) + return len(bad_triggers_occ) < 2 def slugify(self, text): """Normalize a string and remove non-alphanumeric characters. From 3c2479ab4904c566ca978c35bb073a577c73b3a6 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 00:53:58 +0200 Subject: [PATCH 10/30] translate lyrics using Bing API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By subscribing to Microsoft Translator API, one can now activate the translation of lyrics from one set of source langages to a target langage. Translations are appended to each original sentence using ‘/‘ as separator. --- beetsplug/lyrics.py | 56 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index c766db2a7..7b3f93671 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -24,6 +24,7 @@ import unicodedata import urllib import warnings from HTMLParser import HTMLParseError +from langdetect import detect from beets import plugins from beets import ui @@ -567,6 +568,9 @@ class LyricsPlugin(plugins.BeetsPlugin): self.import_stages = [self.imported] self.config.add({ 'auto': True, + 'bing_client_secret': None, + 'bing_lang_from': [], + 'bing_lang_to': None, 'google_API_key': None, 'google_engine_ID': u'009217259823014548361:lndtuqkycfu', 'genius_api_key': @@ -576,6 +580,7 @@ class LyricsPlugin(plugins.BeetsPlugin): 'force': False, 'sources': self.SOURCES, }) + self.config['bing_client_secret'].redact = True self.config['google_API_key'].redact = True self.config['google_engine_ID'].redact = True self.config['genius_api_key'].redact = True @@ -589,6 +594,27 @@ class LyricsPlugin(plugins.BeetsPlugin): self.backends = [self.SOURCE_BACKENDS[key](self.config, self._log) for key in self.config['sources'].as_str_seq()] + self.config['bing_lang_from'] = [ + x.lower() for x in self.config['bing_lang_from'].as_str_seq()] + self.bing_auth_token = None + + def get_bing_access_token(self): + params = { + 'client_id': 'beets', + 'client_secret': self.config['bing_client_secret'], + 'scope': 'http://api.microsofttranslator.com', + 'grant_type': 'client_credentials', + } + + oauth_url = 'https://datamarket.accesscontrol.windows.net/v2/OAuth2-13' + oauth_token = json.loads(requests.post( + oauth_url, + data=urllib.urlencode(params)).content) + if 'access_token' in oauth_token: + return "Bearer " + oauth_token['access_token'] + else: + self._log.warning(u'Could not get Bing Translate API access token. ' + u'Check your "bing_client_secret" password') def commands(self): cmd = ui.Subcommand('lyrics', help='fetch song lyrics') @@ -644,6 +670,15 @@ class LyricsPlugin(plugins.BeetsPlugin): if lyrics: self._log.info(u'fetched lyrics: {0}', item) + lang_lyrics = detect(lyrics) + + if self.config['bing_client_secret'].get() and \ + self.config['bing_lang_to']: + if not self.config['bing_lang_from'] or ( + lang_lyrics in self.config[ + 'bing_lang_from'].as_str_seq()): + lyrics = self.append_translation( + lyrics, self.config['bing_lang_to']) else: self._log.info(u'lyrics not found: {0}', item) fallback = self.config['fallback'].get() @@ -668,3 +703,24 @@ class LyricsPlugin(plugins.BeetsPlugin): self._log.debug(u'got lyrics from backend: {0}', backend.__class__.__name__) return _scrape_strip_cruft(lyrics, True) + + def append_translation(self, text, to_language): + import xml.etree.ElementTree as ET + + if not self.bing_auth_token: + self.bing_auth_token = self.get_bing_access_token() + if self.bing_auth_token: + headers = {"Authorization ": self.bing_auth_token} + url = ('http://api.microsofttranslator.com/v2/Http.svc/' + 'Translate?text=%s&to=%s' % (text.replace('\n', '|'), + to_language)) + r = requests.get(url, headers=headers) + if r.status_code != 200: + self._log.debug(r.text) + return text + translation = ET.fromstring(r.text.encode('utf8')).text + result = '' + for (orig, translated) in zip(text.split('\n'), + translation.split('|')): + result += '%s / %s\n' % (orig, translated) + return result From 6d90dfea2486b30800efd9418c5392406424e366 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 00:54:26 +0200 Subject: [PATCH 11/30] add doc for bing translate api related options --- docs/plugins/lyrics.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/plugins/lyrics.rst b/docs/plugins/lyrics.rst index 0d504733f..c13b02241 100644 --- a/docs/plugins/lyrics.rst +++ b/docs/plugins/lyrics.rst @@ -38,6 +38,14 @@ configuration file. The available options are: - **auto**: Fetch lyrics automatically during import. Default: ``yes``. +- **bing_client_secret**: Your Bing Translation application password (to enable + on-the-fly translating) +- **bing_lang_from**: By default all lyrics with a language other than + ``bing_lang_to`` are translated. Use a list of lang codes to restrict the set + of source languages to translate. + Default: ``[]`` +- **bing_lang_to**: Language to translate lyrics into. + Default: None. - **fallback**: By default, the file will be left unchanged when no lyrics are found. Use the empty string ``''`` to reset the lyrics in such a case. Default: None. @@ -113,3 +121,19 @@ After that, the lyrics plugin will fall back on other declared data sources. .. _pip: http://www.pip-installer.org/ .. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/bs4/doc/ + +Activate on-the-fly translation +------------------------------- + +Using the Bing Translation API requires `langdetect`_, which you can install +using `pip`_ by typing:: + + pip install langdetect + +You also need to register for a Microsoft Azure Marketplace free account and +to the `Microsoft Translator API`_. +Set the ``bing_client_secret`` configuration option to your registered +application password, alongside ``bing_lang_to`` target `language code`_. + +.. _Microsoft Translator API: https://www.microsoft.com/en-us/translator/getstarted.aspx +.. _language code: https://msdn.microsoft.com/en-us/library/hh456380.aspx From 66a627fed8a2c148cd10433e60ff71a27029c0d4 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 00:58:42 +0200 Subject: [PATCH 12/30] restore module docstring --- beetsplug/lyrics.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 7b3f93671..b039bb986 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -13,6 +13,9 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. +"""Fetches, embeds, and displays lyrics. +""" + from __future__ import absolute_import, division, print_function import difflib From e03c3af91f0ee09af42b4b73e3de8057c9c8032a Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 01:11:14 +0200 Subject: [PATCH 13/30] don't translate lyrics already in the target language --- beetsplug/lyrics.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index b039bb986..b18e715d8 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -673,12 +673,11 @@ class LyricsPlugin(plugins.BeetsPlugin): if lyrics: self._log.info(u'fetched lyrics: {0}', item) - lang_lyrics = detect(lyrics) - + lang_from = detect(lyrics) if self.config['bing_client_secret'].get() and \ - self.config['bing_lang_to']: + self.config['bing_lang_to'].get() != lang_from: if not self.config['bing_lang_from'] or ( - lang_lyrics in self.config[ + lang_from in self.config[ 'bing_lang_from'].as_str_seq()): lyrics = self.append_translation( lyrics, self.config['bing_lang_to']) @@ -689,9 +688,7 @@ class LyricsPlugin(plugins.BeetsPlugin): lyrics = fallback else: return - item.lyrics = lyrics - if write: item.try_write() item.store() From 58df77e2cb6dc9bbaabea017d0b9b93809efbbd5 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 08:31:14 +0200 Subject: [PATCH 14/30] langdetect conditional import --- beetsplug/lyrics.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index b18e715d8..40154cefd 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -27,7 +27,6 @@ import unicodedata import urllib import warnings from HTMLParser import HTMLParseError -from langdetect import detect from beets import plugins from beets import ui @@ -673,12 +672,14 @@ class LyricsPlugin(plugins.BeetsPlugin): if lyrics: self._log.info(u'fetched lyrics: {0}', item) - lang_from = detect(lyrics) - if self.config['bing_client_secret'].get() and \ - self.config['bing_lang_to'].get() != lang_from: - if not self.config['bing_lang_from'] or ( + if self.config['bing_client_secret'].get(): + from langdetect import detect + + lang_from = detect(lyrics) + if self.config['bing_lang_to'].get() != lang_from and ( + not self.config['bing_lang_from'] or ( lang_from in self.config[ - 'bing_lang_from'].as_str_seq()): + 'bing_lang_from'].as_str_seq())): lyrics = self.append_translation( lyrics, self.config['bing_lang_to']) else: From 6cfc106b8a4b83e76634eec7fd6cb848fd75b2cc Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 08:31:55 +0200 Subject: [PATCH 15/30] better docs and debug msg --- beetsplug/lyrics.py | 4 +++- docs/plugins/lyrics.rst | 15 +++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 40154cefd..7de1c3d76 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -692,6 +692,7 @@ class LyricsPlugin(plugins.BeetsPlugin): item.lyrics = lyrics if write: item.try_write() + print(lyrics) item.store() def get_lyrics(self, artist, title): @@ -717,7 +718,8 @@ class LyricsPlugin(plugins.BeetsPlugin): to_language)) r = requests.get(url, headers=headers) if r.status_code != 200: - self._log.debug(r.text) + self._log.debug('translation API error {}: {}', r.status_code, + r.text) return text translation = ET.fromstring(r.text.encode('utf8')).text result = '' diff --git a/docs/plugins/lyrics.rst b/docs/plugins/lyrics.rst index c13b02241..e3fa0cc53 100644 --- a/docs/plugins/lyrics.rst +++ b/docs/plugins/lyrics.rst @@ -38,8 +38,8 @@ configuration file. The available options are: - **auto**: Fetch lyrics automatically during import. Default: ``yes``. -- **bing_client_secret**: Your Bing Translation application password (to enable - on-the-fly translating) +- **bing_client_secret**: Your `Bing Translation application password`_ + (to :ref:`lyrics-translation`) - **bing_lang_from**: By default all lyrics with a language other than ``bing_lang_to`` are translated. Use a list of lang codes to restrict the set of source languages to translate. @@ -122,7 +122,9 @@ After that, the lyrics plugin will fall back on other declared data sources. .. _pip: http://www.pip-installer.org/ .. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/bs4/doc/ -Activate on-the-fly translation +.. _lyrics-translation: + +Activate On-the-Fly Translation ------------------------------- Using the Bing Translation API requires `langdetect`_, which you can install @@ -131,9 +133,10 @@ using `pip`_ by typing:: pip install langdetect You also need to register for a Microsoft Azure Marketplace free account and -to the `Microsoft Translator API`_. -Set the ``bing_client_secret`` configuration option to your registered -application password, alongside ``bing_lang_to`` target `language code`_. +to the `Microsoft Translator API`_. Follow the four steps process, specifically +at step 3 enter `beets`` as *Client ID* and copy the generated *Client secret*. +Paste it into your ``bing_client_secret`` configuration, alongside +``bing_lang_to`` target `language code`_. .. _Microsoft Translator API: https://www.microsoft.com/en-us/translator/getstarted.aspx .. _language code: https://msdn.microsoft.com/en-us/library/hh456380.aspx From df46ae6d4d2b909519a7232964498c3cc1cfe394 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 08:38:53 +0200 Subject: [PATCH 16/30] fix docs missing target link --- docs/plugins/lyrics.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/lyrics.rst b/docs/plugins/lyrics.rst index e3fa0cc53..da10bf8f9 100644 --- a/docs/plugins/lyrics.rst +++ b/docs/plugins/lyrics.rst @@ -38,7 +38,7 @@ configuration file. The available options are: - **auto**: Fetch lyrics automatically during import. Default: ``yes``. -- **bing_client_secret**: Your `Bing Translation application password`_ +- **bing_client_secret**: Your Bing Translation application password (to :ref:`lyrics-translation`) - **bing_lang_from**: By default all lyrics with a language other than ``bing_lang_to`` are translated. Use a list of lang codes to restrict the set From d40a168b5bc699d436e7adb19064aa1265aa64f3 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 08:51:24 +0200 Subject: [PATCH 17/30] docs: add langdetect pypi links --- docs/plugins/lyrics.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/lyrics.rst b/docs/plugins/lyrics.rst index da10bf8f9..5cdc3f1c7 100644 --- a/docs/plugins/lyrics.rst +++ b/docs/plugins/lyrics.rst @@ -138,5 +138,6 @@ at step 3 enter `beets`` as *Client ID* and copy the generated *Client secret*. Paste it into your ``bing_client_secret`` configuration, alongside ``bing_lang_to`` target `language code`_. +.. _langdetect: https://pypi.python.org/pypi/langdetect .. _Microsoft Translator API: https://www.microsoft.com/en-us/translator/getstarted.aspx .. _language code: https://msdn.microsoft.com/en-us/library/hh456380.aspx From 0346be701ea4f7ec48ca858bd3bf62d2b4b90008 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Thu, 14 Apr 2016 17:14:49 +0200 Subject: [PATCH 18/30] list skipped tests when running nosetests --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index af245bf8f..d220f1895 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ deps = flask mock nose + nose-show-skipped pyechonest pylast rarfile @@ -29,7 +30,7 @@ deps = {[testenv]deps} coverage commands = - nosetests --with-coverage {posargs} + nosetests --show-skipped --with-coverage {posargs} [testenv:py27setup] basepython = python2.7 From 56d7e5dfa0f1ba232bbcc64556597e6c3f769878 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 22:57:17 +0200 Subject: [PATCH 19/30] send as little text as possible to bing api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bing API has a limit of 2M chars/month. It’s common to have repeating sentences in lyrics so to reduce number of chars sent per song, store sentences in a set and send it, instead of sending the whole lyrics. --- beetsplug/lyrics.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 7de1c3d76..13184f4fe 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -615,8 +615,8 @@ class LyricsPlugin(plugins.BeetsPlugin): if 'access_token' in oauth_token: return "Bearer " + oauth_token['access_token'] else: - self._log.warning(u'Could not get Bing Translate API access token. ' - u'Check your "bing_client_secret" password') + self._log.warning(u'Could not get Bing Translate API access token.' + u' Check your "bing_client_secret" password') def commands(self): cmd = ui.Subcommand('lyrics', help='fetch song lyrics') @@ -706,24 +706,26 @@ class LyricsPlugin(plugins.BeetsPlugin): backend.__class__.__name__) return _scrape_strip_cruft(lyrics, True) - def append_translation(self, text, to_language): + def append_translation(self, text, to_lang): import xml.etree.ElementTree as ET if not self.bing_auth_token: self.bing_auth_token = self.get_bing_access_token() if self.bing_auth_token: - headers = {"Authorization ": 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/' - 'Translate?text=%s&to=%s' % (text.replace('\n', '|'), - to_language)) - r = requests.get(url, headers=headers) + 'Translate?text=%s&to=%s' % ('|'.join(text_lines), to_lang)) + r = requests.get(url, + headers={"Authorization ": self.bing_auth_token}) if r.status_code != 200: self._log.debug('translation API error {}: {}', r.status_code, r.text) return text - translation = ET.fromstring(r.text.encode('utf8')).text + lines_translated = ET.fromstring(r.text.encode('utf8')).text + # Use a translation mapping dict to build resulting lyrics + translations = dict(zip(text_lines, lines_translated.split('|'))) result = '' - for (orig, translated) in zip(text.split('\n'), - translation.split('|')): - result += '%s / %s\n' % (orig, translated) + for line in text.split('\n'): + result += '%s / %s\n' % (line, translations[line]) return result From 05970e8a9391a2f8262738fc60a6ff8df9f6f53f Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 22:57:41 +0200 Subject: [PATCH 20/30] re-query token when it has expired --- beetsplug/lyrics.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 13184f4fe..6a6bc7729 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -721,6 +721,9 @@ class LyricsPlugin(plugins.BeetsPlugin): if r.status_code != 200: self._log.debug('translation API error {}: {}', r.status_code, r.text) + if 'token has expired' in r.text: + self.bing_auth_token = None + return self.append_translation(text, to_lang) return text lines_translated = ET.fromstring(r.text.encode('utf8')).text # Use a translation mapping dict to build resulting lyrics From 95b77bf5d07ae84ac6b5e36f9b1afd5e1f1c59ea Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Thu, 14 Apr 2016 23:24:16 +0200 Subject: [PATCH 21/30] lyrics: update docs + changelog --- docs/changelog.rst | 3 +++ docs/plugins/lyrics.rst | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 67960d56f..81078950e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,9 @@ New features: art while copying it. * :doc:`/plugins/importadded`: A new `preserve_write_mtimes` option lets you preserve mtime of files after each write. +* :doc:`/plugins/lyrics`: The plugin can now translate the fetched lyrics to a + configured `bing_lang_to` langage. Enabling translation require to register + for a Microsoft Azure Marketplace free account. Thanks to :user:`Kraymer`. Fixes: diff --git a/docs/plugins/lyrics.rst b/docs/plugins/lyrics.rst index 5cdc3f1c7..b922b747f 100644 --- a/docs/plugins/lyrics.rst +++ b/docs/plugins/lyrics.rst @@ -134,8 +134,8 @@ using `pip`_ by typing:: You also need to register for a Microsoft Azure Marketplace free account and to the `Microsoft Translator API`_. Follow the four steps process, specifically -at step 3 enter `beets`` as *Client ID* and copy the generated *Client secret*. -Paste it into your ``bing_client_secret`` configuration, alongside +at step 3 enter `beets`` as *Client ID* and copy/paste the generated +*Client secret*. into your ``bing_client_secret`` configuration, alongside ``bing_lang_to`` target `language code`_. .. _langdetect: https://pypi.python.org/pypi/langdetect From ecc6e1c3d6698f800527a9040aecc2d7da1fb712 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Tue, 22 Mar 2016 14:47:09 +0100 Subject: [PATCH 22/30] fanart.tv albumart fetching, missing a project API key --- beetsplug/fetchart.py | 77 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index ad51bb995..61bff901f 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -192,6 +192,68 @@ class GoogleImages(ArtSource): yield item['link'] +class FanartTV(ArtSource): + """Art from fanart.tv requested using their API""" + API_URL = 'http://webservice.fanart.tv/v3/' + API_ALBUMS = API_URL + 'music/albums/' + + def get(self, album): + if not album.mb_releasegroupid: + return + + response = self.request( + self.API_ALBUMS + album.mb_releasegroupid, + headers={ + 'api-key': self._config['fanarttv_api_key'].get(), + 'client-key': self._config['fanarttv_personal_key'].get() + }) + + try: + data = response.json() + except ValueError: + self._log.debug(u'fanart.tv: error loading response: {}', + response.text) + return + + def escapeforlog(s): + # the logger will eventually try to .format() the message, and + # interpret the dict as format spec... + r = [] + for c in s: + if c in ['{', '}']: + r.append(c) + r.append(c) + return ''.join(r) + self._log.debug(escapeforlog(str(data))) + + if u'status' in data and data[u'status'] == u'error': + if u'not found' in data[u'error message'].lower(): + self._log.debug(u'fanart.tv: no image found') + elif u'api key' in data[u'error message'].lower(): + self._log.warning(u'fanart.tv: Invalid API key given, please ' + u'enter a valid one in your config file.') + else: + self._log.debug(u'fanart.tv: error on request: {}', + data[u'error message']) + return + + matches = [] + # can there be more than one releasegroupid per responce? + for mb_releasegroupid in data.get(u'albums', dict()): + if album.mb_releasegroupid == mb_releasegroupid: + # note: there might be more art referenced, e.g. cdart + matches.extend( + data[u'albums'][mb_releasegroupid][u'albumcover']) + # can this actually occur? + else: + self._log.debug(u'fanart.tv: unexpected mb_releasegroupid in ' + u'response!') + + matches.sort(key=lambda x: x[u'likes'], reverse=True) + for item in matches: + yield item[u'url'] + + class ITunesStore(ArtSource): # Art from the iTunes Store. def get(self, album): @@ -351,7 +413,7 @@ class Wikipedia(ArtSource): class FileSystem(ArtSource): - """Art from the filesystem""" + """Art from the fileszystem""" @staticmethod def filename_priority(filename, cover_names): """Sort order for image names. @@ -396,7 +458,7 @@ class FileSystem(ArtSource): # Try each source in turn. SOURCES_ALL = [u'coverart', u'itunes', u'amazon', u'albumart', - u'wikipedia', u'google'] + u'wikipedia', u'google', u'fanarttv'] ART_SOURCES = { u'coverart': CoverArtArchive, @@ -405,6 +467,7 @@ ART_SOURCES = { u'amazon': Amazon, u'wikipedia': Wikipedia, u'google': GoogleImages, + u'fanarttv': FanartTV, } # PLUGIN LOGIC ############################################################### @@ -425,8 +488,11 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): 'sources': ['coverart', 'itunes', 'amazon', 'albumart'], 'google_key': None, 'google_engine': u'001442825323518660753:hrh5ch1gjzm', + 'fanarttv_api_key': None, + 'fanarttv_personal_key': None }) self.config['google_key'].redact = True + self.config['fanarttv_personal_key'].redact = True # Holds paths to downloaded images between fetching them and # placing them in the filesystem. @@ -447,6 +513,13 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if not self.config['google_key'].get() and \ u'google' in available_sources: available_sources.remove(u'google') + if not self.config['fanarttv_personal_key'].get() and \ + u'fanarttv' in available_sources: + self._log.warn( + u'fanart.tv source enabled, but no personal API given. This ' + u'works as of now, however, fanart.tv prefers users to ' + u'register a personal key. Additionaly this makes new art ' + u'available shorter after its upload. See the documentation.') sources_name = plugins.sanitize_choices( self.config['sources'].as_str_seq(), available_sources) self.sources = [ART_SOURCES[s](self._log, self.config) From 8a8b8f832a52fc13bd0a9bc05e2f80e462b7ed5d Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 02:23:22 +0200 Subject: [PATCH 23/30] Update Documentation for fanart.tv --- docs/plugins/fetchart.rst | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index e53dbc219..24a67ded6 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -50,13 +50,15 @@ file. The available options are: - **sources**: List of sources to search for images. An asterisk `*` expands to all available sources. Default: ``coverart itunes amazon albumart``, i.e., everything but - ``wikipedia`` and ``google``. Enable those two sources for more matches at - the cost of some speed. + ``wikipedia``, ``google`` and ``fanarttv``. Enable those sources for more + matches at the cost of some speed. - **google_key**: Your Google API key (to enable the Google Custom Search backend). Default: None. - **google_engine**: The custom search engine to use. Default: The `beets custom search engine`_, which searches the entire web. + **fanarttv_personal_key**: The personal API key for requesting art from + fanart.tv. See below. Note: ``minwidth`` and ``enforce_ratio`` options require either `ImageMagick`_ or `Pillow`_. @@ -162,6 +164,21 @@ default engine searches the entire web for cover art. Note that the Google custom search API is limited to 100 queries per day. After that, the fetchart plugin will fall back on other declared data sources. +Fanart.tv +......... + +Although not strictly necesarry right now, you might think about +`registering a personal fanart.tv API key`_. Set the ``fanarttv_personal_key`` +configurati option to your key, then add ``fanarttv`` to the list of sources in +your configuration. + +.. _registering a personal fanart.tv API key: https://fanart.tv/get-an-api-key/ + +More detailed information can be found `on their blog`_. Specifically, the +personal key will give you earlier access to new art. + +.. _on their blog: https://fanart.tv/2015/01/personal-api-keys/ + Embedding Album Art ------------------- From d46b45861b8f69e76061049fd30e603b00ca6bcb Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 02:40:52 +0200 Subject: [PATCH 24/30] typo, rename project key config option --- beetsplug/fetchart.py | 4 ++-- docs/plugins/fetchart.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 61bff901f..860379107 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -204,7 +204,7 @@ class FanartTV(ArtSource): response = self.request( self.API_ALBUMS + album.mb_releasegroupid, headers={ - 'api-key': self._config['fanarttv_api_key'].get(), + 'api-key': self._config['fanarttv_project_key'].get(), 'client-key': self._config['fanarttv_personal_key'].get() }) @@ -488,7 +488,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): 'sources': ['coverart', 'itunes', 'amazon', 'albumart'], 'google_key': None, 'google_engine': u'001442825323518660753:hrh5ch1gjzm', - 'fanarttv_api_key': None, + 'fanarttv_project_key': None, 'fanarttv_personal_key': None }) self.config['google_key'].redact = True diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 24a67ded6..f9280204b 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -169,8 +169,8 @@ Fanart.tv Although not strictly necesarry right now, you might think about `registering a personal fanart.tv API key`_. Set the ``fanarttv_personal_key`` -configurati option to your key, then add ``fanarttv`` to the list of sources in -your configuration. +configuration option to your key, then add ``fanarttv`` to the list of sources +in your configuration. .. _registering a personal fanart.tv API key: https://fanart.tv/get-an-api-key/ From 2dfdc8b90ad260d4ac4ec367ff9bf4bc0b70fc26 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 02:47:56 +0200 Subject: [PATCH 25/30] fix doc formatting --- docs/plugins/fetchart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index f9280204b..005bea320 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -165,7 +165,7 @@ Note that the Google custom search API is limited to 100 queries per day. After that, the fetchart plugin will fall back on other declared data sources. Fanart.tv -......... +''''''''' Although not strictly necesarry right now, you might think about `registering a personal fanart.tv API key`_. Set the ``fanarttv_personal_key`` From 87aa5dab13e3605b69a67ab13bd4592b54400119 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 13:28:48 +0200 Subject: [PATCH 26/30] fixes according to feedback by @Kraymer --- beetsplug/fetchart.py | 25 ++++++++++--------------- docs/plugins/fetchart.rst | 6 +++--- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 860379107..fd84416d5 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -196,6 +196,7 @@ class FanartTV(ArtSource): """Art from fanart.tv requested using their API""" API_URL = 'http://webservice.fanart.tv/v3/' API_ALBUMS = API_URL + 'music/albums/' + PROJECT_KEY = '' def get(self, album): if not album.mb_releasegroupid: @@ -204,8 +205,8 @@ class FanartTV(ArtSource): response = self.request( self.API_ALBUMS + album.mb_releasegroupid, headers={ - 'api-key': self._config['fanarttv_project_key'].get(), - 'client-key': self._config['fanarttv_personal_key'].get() + 'api-key': self.PROJECT_KEY, + 'client-key': self._config['fanarttv_key'].get() }) try: @@ -215,16 +216,11 @@ class FanartTV(ArtSource): response.text) return - def escapeforlog(s): + def escape_for_log(s): # the logger will eventually try to .format() the message, and # interpret the dict as format spec... - r = [] - for c in s: - if c in ['{', '}']: - r.append(c) - r.append(c) - return ''.join(r) - self._log.debug(escapeforlog(str(data))) + return ''.join((2 * c if c in '{}' else c for c in s)) + self._log.debug(escape_for_log(str(data))) if u'status' in data and data[u'status'] == u'error': if u'not found' in data[u'error message'].lower(): @@ -413,7 +409,7 @@ class Wikipedia(ArtSource): class FileSystem(ArtSource): - """Art from the fileszystem""" + """Art from the filesystem""" @staticmethod def filename_priority(filename, cover_names): """Sort order for image names. @@ -488,11 +484,10 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): 'sources': ['coverart', 'itunes', 'amazon', 'albumart'], 'google_key': None, 'google_engine': u'001442825323518660753:hrh5ch1gjzm', - 'fanarttv_project_key': None, - 'fanarttv_personal_key': None + 'fanarttv_key': None }) self.config['google_key'].redact = True - self.config['fanarttv_personal_key'].redact = True + self.config['fanarttv_key'].redact = True # Holds paths to downloaded images between fetching them and # placing them in the filesystem. @@ -513,7 +508,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if not self.config['google_key'].get() and \ u'google' in available_sources: available_sources.remove(u'google') - if not self.config['fanarttv_personal_key'].get() and \ + if not self.config['fanarttv_key'].get() and \ u'fanarttv' in available_sources: self._log.warn( u'fanart.tv source enabled, but no personal API given. This ' diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 005bea320..f5e3ae601 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -57,7 +57,7 @@ file. The available options are: Default: None. - **google_engine**: The custom search engine to use. Default: The `beets custom search engine`_, which searches the entire web. - **fanarttv_personal_key**: The personal API key for requesting art from + **fanarttv_key**: The personal API key for requesting art from fanart.tv. See below. Note: ``minwidth`` and ``enforce_ratio`` options require either `ImageMagick`_ @@ -167,8 +167,8 @@ After that, the fetchart plugin will fall back on other declared data sources. Fanart.tv ''''''''' -Although not strictly necesarry right now, you might think about -`registering a personal fanart.tv API key`_. Set the ``fanarttv_personal_key`` +Although not strictly necessary right now, you might think about +`registering a personal fanart.tv API key`_. Set the ``fanarttv_key`` configuration option to your key, then add ``fanarttv`` to the list of sources in your configuration. From 7bec3b9de5d9cb7ab5b857a4bc1bd022a39887ca Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 14:56:14 +0200 Subject: [PATCH 27/30] fanart.tv tests, mostly copied and pasted from the google tests --- test/test_art.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/test/test_art.py b/test/test_art.py index ebfcd53f4..79326b6c4 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -260,6 +260,81 @@ class GoogleImageTest(UseThePlugin): self.assertEqual(list(result_url), []) +class FanartTVTest(UseThePlugin): + RESPONSE_MULTIPLE = u"""{ + "name": "artistname", + "mbid_id": "artistid", + "albums": { + "thereleasegroupid": { + "albumcover": [ + { + "id": "24", + "url": "http://example.com/1.jpg", + "likes": "0" + }, + { + "id": "42", + "url": "http://example.com/2.jpg", + "likes": "0" + }, + { + "id": "23", + "url": "http://example.com/3.jpg", + "likes": "0" + } + ], + "cdart": [ + { + "id": "123", + "url": "http://example.com/4.jpg", + "likes": "0", + "disc": "1", + "size": "1000" + } + ] + } + } + }""" + RESPONSE_ERROR = u"""{ + "status": "error", + "error message": "the error message" + }""" + RESPONSE_MALFORMED = u"bla blup" + + def setUp(self): + super(FanartTVTest, self).setUp() + self.source = fetchart.FanartTV(logger, self.plugin.config) + + @responses.activate + def run(self, *args, **kwargs): + super(FanartTVTest, self).run(*args, **kwargs) + + def mock_response(self, url, json): + responses.add(responses.GET, url, body=json, + content_type='application/json') + + def test_fanarttv_finds_image(self): + album = _common.Bag(mb_releasegroupid=u'thereleasegroupid') + self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', + self.RESPONSE_MULTIPLE) + result_url = self.source.get(album) + self.assertEqual(list(result_url)[0], 'http://example.com/1.jpg') + + def test_fanarttv_returns_no_result_when_error_received(self): + album = _common.Bag(mb_releasegroupid=u'thereleasegroupid') + self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', + self.RESPONSE_ERROR) + result_url = self.source.get(album) + self.assertEqual(list(result_url), []) + + 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) + result_url = self.source.get(album) + self.assertEqual(list(result_url), []) + + @_common.slow_test() class ArtImporterTest(UseThePlugin): def setUp(self): From 3a2eb03e0e377c80b9e1191b60c3978e1dd9fdea Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 19:32:17 +0200 Subject: [PATCH 28/30] Add fanart.tv API key --- beetsplug/fetchart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index fd84416d5..a31e25cca 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -196,7 +196,7 @@ class FanartTV(ArtSource): """Art from fanart.tv requested using their API""" API_URL = 'http://webservice.fanart.tv/v3/' API_ALBUMS = API_URL + 'music/albums/' - PROJECT_KEY = '' + PROJECT_KEY = '61a7d0ab4e67162b7a0c7c35915cd48e' def get(self, album): if not album.mb_releasegroupid: From 4763fec35b9efe088785dc87928ef8630755d31f Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 19:42:06 +0200 Subject: [PATCH 29/30] fanart.tv: remove overly verbose logging left over from debugging --- beetsplug/fetchart.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index a31e25cca..6f38f6bd2 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -216,12 +216,6 @@ class FanartTV(ArtSource): response.text) return - def escape_for_log(s): - # the logger will eventually try to .format() the message, and - # interpret the dict as format spec... - return ''.join((2 * c if c in '{}' else c for c in s)) - self._log.debug(escape_for_log(str(data))) - if u'status' in data and data[u'status'] == u'error': if u'not found' in data[u'error message'].lower(): self._log.debug(u'fanart.tv: no image found') From 9106f2d1c27be45c1644079ff153645aa579c838 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 20:14:45 +0200 Subject: [PATCH 30/30] update changelog --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 67960d56f..9f18dda0d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,10 @@ New features: art while copying it. * :doc:`/plugins/importadded`: A new `preserve_write_mtimes` option lets you preserve mtime of files after each write. +* :doc:`/plugins/fetchart`: Album art can now be fetched from `fanart.tv`_. + Albums are matched using the ``mb_releasegroupid`` tag. + +.. _fanart.tv: https://fanart.tv/ Fixes: