Skip to content

Commit

Permalink
Upload Lrclib Lyrics plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
twodoorcoupe committed Oct 29, 2024
1 parent 437b7a6 commit 18d2f7e
Show file tree
Hide file tree
Showing 3 changed files with 382 additions and 0 deletions.
210 changes: 210 additions & 0 deletions plugins/lrclib_lyrics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2024 Giorgio Fontanive (twodoorcoupe)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

PLUGIN_NAME = "Lrclib Lyrics"
PLUGIN_AUTHOR = "Giorgio Fontanive"
PLUGIN_DESCRIPTION = """
Fetches lyrics from lrclib.net
Also allows to export lyrics to an .lrc file or import them from one.
"""
PLUGIN_VERSION = "0.1"
PLUGIN_API_VERSIONS = ["2.12"]
PLUGIN_LICENSE = "GPL-2.0"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

import os
import re
from functools import partial

from picard import config, log
from picard.file import register_file_post_save_processor, register_file_post_addition_to_track_processor
from picard.track import Track
from picard.ui.itemviews import BaseAction, register_track_action
from picard.ui.options import OptionsPage, register_options_page
from picard.webservice import ratecontrol

from .option_lrclib_lyrics import Ui_OptionLrclibLyrics


URL = "https://lrclib.net/api/get"
REQUESTS_DELAY = 100

# Options
ADD_LYRICS = "add_lyrics"
ADD_SYNCEDLYRICS = "add_syncedlyrics"
NEVER_REPLACE_LYRICS = "never_replace_lyrics"
LRC_FILENAME = "exported_lrc_filename"
EXPORT_LRC = "exported_lrc"
NEVER_REPLACE_LRC = "never_replace_lrc"

lyrics_cache = {}
synced_lyrics_pattern = re.compile(r"(\[\d\d:\d\d\.\d\d\d]|<\d\d:\d\d\.\d\d\d>)")
tags_pattern = re.compile(r"%(\w+)%")
extra_file_variables = {
"filepath": lambda file: file,
"folderpath": lambda file: os.path.dirname(file),
"filename": lambda file: os.path.splitext(os.path.basename(file))[0],
"filename_ext": lambda file: os.path.basename(file),
"directory": lambda file: os.path.basename(os.path.dirname(file))
}


def get_lyrics(track, file):
album = track.album
metadata = file.metadata
if not (config.setting[ADD_LYRICS] or config.setting[ADD_SYNCEDLYRICS]):
return
if not (metadata.get("title") and metadata.get("artist")):
log.debug(f"Skipping fetching lyrics for track in {album} as both title and artist are required")
return
if config.setting[NEVER_REPLACE_LYRICS] and metadata.get("lyrics"):
log.debug(f"Skipping fetching lyrics for {metadata["title"]} as lyrics are already embedded")

Check notice on line 73 in plugins/lrclib_lyrics/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

plugins/lrclib_lyrics/__init__.py#L73

f-string: unmatched '[' (F999)
return
args = {
"track_name": metadata["title"],
"artist_name": metadata["artist"],
}
if metadata.get("album"):
args["album_name"] = metadata["album"]
handler = partial(response_handler, album, metadata)
album._requests += 1
album.tagger.webservice.get_url(
method="GET",
handler=handler,
parse_response_type='json',
url=URL,
unencoded_queryargs=args
)


def response_handler(album, metadata, document, reply, error):
if document and not error:
lyrics = document.get("plainLyrics")
syncedlyrics = document.get("syncedLyrics")
if lyrics:
lyrics_cache[metadata["title"]] = lyrics
if ((not config.setting[ADD_LYRICS]) or
(config.setting[NEVER_REPLACE_LYRICS] and metadata.get("lyrics"))):
return
metadata["lyrics"] = lyrics
if syncedlyrics:
lyrics_cache[metadata["title"]] = syncedlyrics
# Support for the syncedlyrics tag is not available yet
# if (not config.setting[ADD_SYNCEDLYRICS] or
# (config.setting[NEVER_REPLACE_LYRICS] and metadata.get("syncedlyrics"))):
# return
# metadata["syncedlyrics"] = syncedlyrics
else:
log.debug(f"Could not fetch lyrics for {metadata["title"]}")
album._requests -= 1
album._finalize_loading(None)


def get_lrc_file_name(file):
filename = f"{tags_pattern.sub("{}", config.setting[LRC_FILENAME])}"
tags = tags_pattern.findall(config.setting[LRC_FILENAME])
values = []
for tag in tags:
if tag in extra_file_variables:
values.append(extra_file_variables[tag](file.filename))
else:
values.append(file.metadata.get(tag, f"%{tag}%"))
return filename.format(*values)


def export_lrc_file(file):
if config.setting[EXPORT_LRC]:
metadata = file.metadata
# If no lyrics were downloaded, try to export the lyrics already embedded
lyrics = lyrics_cache.pop(metadata["title"], metadata.get("lyrics"))
if lyrics:
filename = get_lrc_file_name(file)
if config.setting[NEVER_REPLACE_LRC] and os.path.exists(filename):
return
try:
with open(filename, 'w') as file:
file.write(lyrics)
log.debug(f"Created new lyrics file at {filename}")
except OSError:
log.debug(f"Could not create the lrc file for {metadata["title"]}")
else:
log.debug(f"Could not export any lyrics for {metadata["title"]}")


class ImportLrc(BaseAction):
NAME = 'Import lyrics from lrc files'

def callback(self, objs):
for track in objs:
if isinstance(track, Track):
file = track.files[0]
filename = get_lrc_file_name(file)
try:
with open(filename, 'r') as lyrics_file:
lyrics = lyrics_file.read()
if synced_lyrics_pattern.search(lyrics):
# Support for syncedlyrics is not available yet
# file.metadata["syncedlyrics"] = lyrics
pass
else:
file.metadata["lyrics"] = lyrics
except FileNotFoundError:
log.debug(f"Could not find matching lrc file for {file.metadata["title"]}")


class LrclibLyricsOptions(OptionsPage):

NAME = "lrclib_lyrics"
TITLE = "Lrclib Lyrics"
PARENT = "plugins"

__default_naming = "%filename%.lrc"

options = [
config.BoolOption("setting", ADD_LYRICS, True),
config.BoolOption("setting", ADD_SYNCEDLYRICS, False),
config.BoolOption("setting", NEVER_REPLACE_LYRICS, False),
config.TextOption("setting", LRC_FILENAME, __default_naming),
config.BoolOption("setting", EXPORT_LRC, False),
config.BoolOption("setting", NEVER_REPLACE_LRC, False),
]

def __init__(self, parent=None):
super(LrclibLyricsOptions, self).__init__(parent)
self.ui = Ui_OptionLrclibLyrics()
self.ui.setupUi(self)

def load(self):
self.ui.lyrics.setChecked(config.setting[ADD_LYRICS])
self.ui.syncedlyrics.setChecked(config.setting[ADD_SYNCEDLYRICS])
self.ui.replace_embedded.setChecked(config.setting[NEVER_REPLACE_LYRICS])
self.ui.lrc_name.setText(config.setting[LRC_FILENAME])
self.ui.export_lyrics.setChecked(config.setting[EXPORT_LRC])
self.ui.replace_exported.setChecked(config.setting[NEVER_REPLACE_LRC])

def save(self):
config.setting[ADD_LYRICS] = self.ui.lyrics.isChecked()
config.setting[ADD_SYNCEDLYRICS] = self.ui.syncedlyrics.isChecked()
config.setting[NEVER_REPLACE_LYRICS] = self.ui.replace_embedded.isChecked()
config.setting[LRC_FILENAME] = self.ui.lrc_name.text()
config.setting[EXPORT_LRC] = self.ui.export_lyrics.isChecked()
config.setting[NEVER_REPLACE_LRC] = self.ui.replace_exported.isChecked()


ratecontrol.set_minimum_delay_for_url(URL, REQUESTS_DELAY)
register_file_post_addition_to_track_processor(get_lyrics)
register_file_post_save_processor(export_lrc_file)
register_track_action(ImportLrc())
register_options_page(LrclibLyricsOptions)
69 changes: 69 additions & 0 deletions plugins/lrclib_lyrics/option_lrclib_lyrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'plugins/lrclib_lyrics/option_lrclib_lyrics.ui'
#
# Created by: PyQt5 UI code generator 5.15.10
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets

Check warning on line 11 in plugins/lrclib_lyrics/option_lrclib_lyrics.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

plugins/lrclib_lyrics/option_lrclib_lyrics.py#L11

Unused QtGui imported from PyQt5


class Ui_OptionLrclibLyrics(object):
def setupUi(self, OptionLrclibLyrics):
OptionLrclibLyrics.setObjectName("OptionLrclibLyrics")
OptionLrclibLyrics.resize(432, 368)
self.verticalLayout = QtWidgets.QVBoxLayout(OptionLrclibLyrics)
self.verticalLayout.setObjectName("verticalLayout")
self.groupBox = QtWidgets.QGroupBox(OptionLrclibLyrics)
self.groupBox.setObjectName("groupBox")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBox)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.lyrics = QtWidgets.QCheckBox(self.groupBox)
self.lyrics.setObjectName("lyrics")
self.verticalLayout_2.addWidget(self.lyrics)
self.syncedlyrics = QtWidgets.QCheckBox(self.groupBox)
self.syncedlyrics.setEnabled(False)
self.syncedlyrics.setCheckable(False)
self.syncedlyrics.setObjectName("syncedlyrics")
self.verticalLayout_2.addWidget(self.syncedlyrics)
self.replace_embedded = QtWidgets.QCheckBox(self.groupBox)
self.replace_embedded.setObjectName("replace_embedded")
self.verticalLayout_2.addWidget(self.replace_embedded)
self.verticalLayout.addWidget(self.groupBox)
self.groupBox_2 = QtWidgets.QGroupBox(OptionLrclibLyrics)
self.groupBox_2.setObjectName("groupBox_2")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.groupBox_2)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.label = QtWidgets.QLabel(self.groupBox_2)
self.label.setObjectName("label")
self.verticalLayout_3.addWidget(self.label)
self.lrc_name = QtWidgets.QLineEdit(self.groupBox_2)
self.lrc_name.setObjectName("lrc_name")
self.verticalLayout_3.addWidget(self.lrc_name)
self.export_lyrics = QtWidgets.QCheckBox(self.groupBox_2)
self.export_lyrics.setObjectName("export_lyrics")
self.verticalLayout_3.addWidget(self.export_lyrics)
self.replace_exported = QtWidgets.QCheckBox(self.groupBox_2)
self.replace_exported.setObjectName("replace_exported")
self.verticalLayout_3.addWidget(self.replace_exported)
self.verticalLayout.addWidget(self.groupBox_2)
spacerItem = QtWidgets.QSpacerItem(20, 23, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)

self.retranslateUi(OptionLrclibLyrics)
QtCore.QMetaObject.connectSlotsByName(OptionLrclibLyrics)

def retranslateUi(self, OptionLrclibLyrics):
_translate = QtCore.QCoreApplication.translate
OptionLrclibLyrics.setWindowTitle(_translate("OptionLrclibLyrics", "Form"))
self.groupBox.setTitle(_translate("OptionLrclibLyrics", "Embedded Lyrics Options"))
self.lyrics.setText(_translate("OptionLrclibLyrics", "Download and embed lyrics"))
self.syncedlyrics.setText(_translate("OptionLrclibLyrics", "Download and embed synchronized lyrics"))
self.replace_embedded.setText(_translate("OptionLrclibLyrics", "Never replace embedded lyrics if already present"))
self.groupBox_2.setTitle(_translate("OptionLrclibLyrics", "Lrc Files Options"))
self.label.setText(_translate("OptionLrclibLyrics", "Use the following name pattern for lrc files:"))
self.export_lyrics.setText(_translate("OptionLrclibLyrics", "Export lyrics to lrc file when saving"))
self.replace_exported.setText(_translate("OptionLrclibLyrics", "Never replace lrc files if already present"))
103 changes: 103 additions & 0 deletions plugins/lrclib_lyrics/option_lrclib_lyrics.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OptionLrclibLyrics</class>
<widget class="QWidget" name="OptionLrclibLyrics">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>432</width>
<height>368</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Embedded Lyrics Options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="lyrics">
<property name="text">
<string>Download and embed lyrics</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="syncedlyrics">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Download and embed synchronized lyrics</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="replace_embedded">
<property name="text">
<string>Never replace embedded lyrics if already present</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Lrc Files Options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Use the following name pattern for lrc files:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lrc_name"/>
</item>
<item>
<widget class="QCheckBox" name="export_lyrics">
<property name="text">
<string>Export lyrics to lrc file when saving</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="replace_exported">
<property name="text">
<string>Never replace lrc files if already present</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>23</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

0 comments on commit 18d2f7e

Please sign in to comment.