diff --git a/plugins/happidev_lyrics/happidev_lyrics.py b/plugins/happidev_lyrics/happidev_lyrics.py
new file mode 100644
index 00000000..9ff0f211
--- /dev/null
+++ b/plugins/happidev_lyrics/happidev_lyrics.py
@@ -0,0 +1,170 @@
+from functools import partial
+from urllib.parse import (
+ quote,
+ urlencode,
+ urlparse,
+)
+
+from PyQt5 import QtWidgets
+from PyQt5.QtNetwork import QNetworkRequest
+
+from picard import (
+ config,
+ log,
+)
+from picard.config import TextOption
+from picard.metadata import register_track_metadata_processor
+from picard.ui.options import (
+ OptionsPage,
+ register_options_page,
+)
+from picard.webservice import ratecontrol
+
+PLUGIN_NAME = 'Happi.dev Lyrics'
+PLUGIN_AUTHOR = 'Andrea Avallone, Philipp Wolfer'
+PLUGIN_DESCRIPTION = 'Fetch lyrics from Happi.dev Lyrics, which provides millions of lyrics from artist all around the world. ' \
+ 'Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed.
' \
+ 'In order to use Happi.dev you need to get a free API key at happi.dev'
+PLUGIN_VERSION = '2.1.2'
+PLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.3', '2.4', '2.5', '2.6']
+PLUGIN_LICENSE = 'MIT'
+PLUGIN_LICENSE_URL = 'https://opensource.org/licenses/MIT'
+
+
+class HappidevLyricsMetadataProcessor:
+
+ happidev_host = 'api.happi.dev'
+ happidev_port = 443
+ happidev_delay = int(60 * 1000 / 60) # 60 requests per minute
+
+ def __init__(self):
+ super().__init__()
+ ratecontrol.set_minimum_delay(
+ (self.happidev_host, self.happidev_port), self.happidev_delay)
+
+ def process_metadata(self, album, metadata, track, release):
+ if not config.setting['happidev_apikey']:
+ error = 'API key is missing, please provide a valid value'
+ log.warning('{}: {}'.format(PLUGIN_NAME, error))
+ return
+
+ artist = metadata['artist']
+ title = metadata['title']
+ if not (artist and title):
+ log.debug(
+ '{}: both artist and title are required to obtain lyrics'.format(PLUGIN_NAME))
+ return
+
+ path = '/v1/music'
+ queryargs = {
+ 'q': '"{}" "{}"'.format(artist, title),
+ 'lyrics': 'true',
+ 'type': 'track',
+ }
+ album._requests += 1
+ log.debug('{}: GET {}?{}'.format(PLUGIN_NAME, quote(path), urlencode(queryargs)))
+ self._request(album.tagger.webservice, path,
+ partial(self.process_search_response, album, metadata), queryargs)
+
+ def _request(self, ws, path, callback, queryargs=None, important=False):
+ if not queryargs:
+ queryargs = {}
+
+ queryargs['apikey'] = config.setting['happidev_apikey']
+ ws.get(self.happidev_host, self.happidev_port, path, callback,
+ parse_response_type='json', priority=True, important=important,
+ queryargs=queryargs, cacheloadcontrol=QNetworkRequest.PreferCache)
+
+ def process_search_response(self, album, metadata, response, reply, error):
+ if self._handle_error(album, error, response):
+ log.warning('{}: lyrics NOT found for track "{}" by {}'.format(
+ PLUGIN_NAME, metadata['title'], metadata['artist']))
+ return
+
+ try:
+ lyrics_url = response['result'][0]['api_lyrics']
+ log.debug('{}: lyrics found for track "{}" by {} at {}'.format(
+ PLUGIN_NAME, metadata['title'], metadata['artist'], lyrics_url))
+ path = urlparse(lyrics_url).path
+
+ except (TypeError, KeyError, ValueError):
+ log.warn('{}: failed parsing search response for "{}" by {}'.format(
+ PLUGIN_NAME, metadata['title'], metadata['artist']), exc_info=True)
+ album._requests -= 1
+ album._finalize_loading(None)
+ return
+
+ self._request(album.tagger.webservice, path,
+ partial(self.process_lyrics_response, album, metadata),
+ important=True)
+
+ def process_lyrics_response(self, album, metadata, response, reply, error):
+ if self._handle_error(album, error, response):
+ log.warning('{}: lyrics NOT loaded for track "{}" by {}'.format(
+ PLUGIN_NAME, metadata['title'], metadata['artist']))
+ return
+
+ try:
+ lyrics = response['result']['lyrics']
+ metadata['lyrics'] = lyrics
+ log.debug('{}: lyrics loaded for track "{}" by {}'.format(
+ PLUGIN_NAME, metadata['title'], metadata['artist']))
+
+ except (TypeError, KeyError):
+ log.warn('{}: failed parsing search response for "{}" by {}'.format(
+ PLUGIN_NAME, metadata['title'], metadata['artist']), exc_info=True)
+
+ finally:
+ album._requests -= 1
+ album._finalize_loading(None)
+
+ @staticmethod
+ def _handle_error(album, error, response):
+ if error or (response and (not response.get('success', False) or not response.get('length', 0))):
+ album._requests -= 1
+ album._finalize_loading(None)
+ return True
+
+ return False
+
+
+class HappidevLyricsOptionsPage(OptionsPage):
+
+ NAME = 'apiseeds_lyrics'
+ TITLE = 'Happi.dev Lyrics'
+ PARENT = 'plugins'
+
+ options = [TextOption('setting', 'happidev_apikey', '')]
+
+ def __init__(self, parent=None):
+
+ super().__init__(parent)
+ self.box = QtWidgets.QVBoxLayout(self)
+
+ self.label = QtWidgets.QLabel(self)
+ self.label.setText('Apiseeds API key')
+ self.box.addWidget(self.label)
+
+ self.description = QtWidgets.QLabel(self)
+ self.description.setText('Happi.dev Music provides millions of lyrics from artist all around the world. '
+ 'Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. '
+ 'In order to use Happi.dev Music you need to get a free API key here.')
+ self.description.setOpenExternalLinks(True)
+ self.box.addWidget(self.description)
+
+ self.input = QtWidgets.QLineEdit(self)
+ self.box.addWidget(self.input)
+
+ self.spacer = QtWidgets.QSpacerItem(
+ 0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+ self.box.addItem(self.spacer)
+
+ def load(self):
+ self.input.setText(config.setting['happidev_apikey'])
+
+ def save(self):
+ config.setting['happidev_apikey'] = self.input.text()
+
+
+register_track_metadata_processor(HappidevLyricsMetadataProcessor().process_metadata)
+register_options_page(HappidevLyricsOptionsPage)
\ No newline at end of file