Skip to content

Instantly share code, notes, and snippets.

@Dylancyclone
Last active July 21, 2024 13:24
Show Gist options
  • Save Dylancyclone/72fdb3515ed6b4150365c46fe07f4de8 to your computer and use it in GitHub Desktop.
Save Dylancyclone/72fdb3515ed6b4150365c46fe07f4de8 to your computer and use it in GitHub Desktop.
MusicBrainz Picard LRCLIB Lyrics Plugin
from functools import partial
from urllib.parse import (
quote,
urlencode,
)
from PyQt5.QtNetwork import QNetworkRequest
from picard import config, log
from picard.metadata import register_track_metadata_processor
from picard.track import Track
from picard.album import Album
from picard.ui.itemviews import (
BaseAction,
register_track_action,
register_album_action,
)
from PyQt5 import QtWidgets
from picard.ui.options import (
OptionsPage,
register_options_page,
)
from picard.config import BoolOption
PLUGIN_NAME = "LRCLIB Lyrics"
PLUGIN_AUTHOR = "Dylancyclone"
PLUGIN_DESCRIPTION = (
"Fetch lyrics from LRCLIB.<br/>"
"Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed."
)
PLUGIN_VERSION = "1.0.0"
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"
lrclib_url = "https://lrclib.net/api/get"
def _request(ws, callback, queryargs=None, important=False):
if not queryargs:
queryargs = {}
ws.get_url(
url=lrclib_url,
handler=callback,
parse_response_type="json",
priority=True,
important=important,
queryargs=queryargs,
cacheloadcontrol=QNetworkRequest.PreferCache,
)
def search_for_lyrics(album, metadata, length, linked_files):
artist = metadata["artist"]
title = metadata["title"]
albumName = metadata["album"]
duration = int(length)
if not (artist and title and albumName and duration):
log.debug(
"{}: artist, title, album name, and duration are required to obtain lyrics".format(
PLUGIN_NAME
)
)
return
queryargs = {
"track_name": title,
"artist_name": artist,
"album_name": albumName,
"duration": duration,
}
album._requests += 1
log.debug(
"{}: GET {}?{}".format(PLUGIN_NAME, quote(lrclib_url), urlencode(queryargs))
)
_request(
album.tagger.webservice,
partial(process_search_response, album, metadata, linked_files),
queryargs,
)
def process_search_response(album, metadata, linked_files, response, reply, error):
if error or (response and not response.get("id", False)):
album._requests -= 1
album._finalize_loading(None)
log.warning(
'{}: lyrics NOT found for track "{}" by {}'.format(
PLUGIN_NAME, metadata["title"], metadata["artist"]
)
)
return
try:
lyrics = (
response["syncedLyrics"]
if response.get("syncedLyrics")
else response["plainLyrics"]
)
metadata["lyrics"] = lyrics
if linked_files:
for file in linked_files:
file.metadata["lyrics"] = lyrics
log.debug(
'{}: lyrics loaded for track "{}" by {}'.format(
PLUGIN_NAME, metadata["title"], metadata["artist"]
)
)
except (TypeError, KeyError, ValueError):
log.warn(
'{}: lyrics NOT loaded for track "{}" by {}'.format(
PLUGIN_NAME, metadata["title"], metadata["artist"]
),
exc_info=True,
)
finally:
album._requests -= 1
album._finalize_loading(None)
class LrclibLyricsOptionsPage(OptionsPage):
NAME = "lrclib_lyrics"
TITLE = "LRCLIB Lyrics"
PARENT = "plugins"
options = [BoolOption("setting", "search_on_load", False)]
def __init__(self, parent=None):
super().__init__(parent)
self.box = QtWidgets.QVBoxLayout(self)
self.input = QtWidgets.QCheckBox("Search for lyrics when loading tracks", self)
self.box.addWidget(self.input)
self.spacer = QtWidgets.QSpacerItem(
0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding
)
self.box.addItem(self.spacer)
self.description = QtWidgets.QLabel(self)
self.description.setText(
"LRCLIB Music provides millions of lyrics from artist all around the world.\n"
"Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed.\n"
"If searching for lyrics when loading tracks, the loading process will be slowed significantly."
)
self.description.setOpenExternalLinks(True)
self.box.addWidget(self.description)
def load(self):
self.input.setChecked(config.setting["search_on_load"])
def save(self):
config.setting["search_on_load"] = self.input.checkState()
class LrclibLyricsMetadataProcessor:
def __init__(self):
super().__init__()
def process_metadata(self, album, metadata, track, release):
if config.setting["search_on_load"]:
search_for_lyrics(album, metadata, int(track["length"]) / 1000)
class LrclibLyricsTrackAction(BaseAction):
NAME = "Search for lyrics with LRCLIB..."
def execute_on_track(self, track):
length = track.metadata["~length"].split(":")
length = int(length[0]) * 60 + int(length[1])
search_for_lyrics(track.album, track.metadata, length, track.linked_files)
def callback(self, objs):
for item in (t for t in objs if isinstance(t, Track) or isinstance(t, Album)):
if isinstance(item, Track):
log.debug("{}: {}, {}".format(PLUGIN_NAME, item, item.album))
self.execute_on_track(item)
elif isinstance(item, Album):
for track in item.tracks:
log.debug("{}: {}, {}".format(PLUGIN_NAME, track, item))
self.execute_on_track(track)
register_track_metadata_processor(LrclibLyricsMetadataProcessor().process_metadata)
register_track_action(LrclibLyricsTrackAction())
register_album_action(LrclibLyricsTrackAction())
register_options_page(LrclibLyricsOptionsPage)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment