diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 0a34fa60e8..0e7c986cb8 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -31,6 +31,7 @@ import requests from bs4 import BeautifulSoup +from confuse import Optional from unidecode import unidecode from beets import plugins, ui @@ -1052,6 +1053,7 @@ def __init__(self): "keep_synced": False, "local": False, "print": False, + "rest_directory": None, "synced": False, # Musixmatch and Tekstowo are disabled by default as they # currently block requests with the beets user agent. @@ -1084,7 +1086,7 @@ def commands(self): "--write-rest", dest="rest_directory", action="store", - default=None, + default=self.config["rest_directory"].get(Optional(str)), metavar="dir", help="write lyrics to given directory as ReST files", ) @@ -1122,7 +1124,7 @@ def func(lib: Library, opts, args) -> None: if opts.rest_directory and ( items := [i for i in items if i.lyrics] ): - RestFiles(Path(opts.rest_directory)).write(items) + RestFiles(Path(opts.rest_directory).expanduser()).write(items) cmd.func = func return [cmd] diff --git a/docs/changelog.rst b/docs/changelog.rst index 7ffe7c5d0e..df7ae9588c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,9 @@ New features :conf:`plugins.musicbrainz:aliases_as_credits` to make aliases-as-artist-credit optional. - :doc:`plugins/badfiles`: Added settings for auto error and warning actions. +- :doc:`plugins/lyrics`: Added a ``rest_directory`` configuration option for + specifying a reStructuredText output directory, semantically equivalent to + ``-r, --write-rest``. :bug:`2806` - :doc:`plugins/tidal`: New flexible attributes are now populated during imports, including ``tidal_track_id``, ``tidal_album_id``, ``tidal_artist_id``, ``tidal_track_popularity``, ``tidal_album_popularity``, diff --git a/docs/plugins/lyrics.rst b/docs/plugins/lyrics.rst index ae83ffdc14..8fa2b4c1db 100644 --- a/docs/plugins/lyrics.rst +++ b/docs/plugins/lyrics.rst @@ -59,6 +59,7 @@ Default configuration: google_API_key: null google_engine_ID: 009217259823014548361:lndtuqkycfu print: no + rest_directory: null sources: [lrclib, google, genius] synced: no @@ -107,6 +108,8 @@ The available options are: custom search engine`_, which gathers an updated list of sources known to be scrapeable. - **print**: Print lyrics to the console. +- **rest_directory**: The directory to which reStructuredText_ (ReST) rendered + lyric documents will be output. See :ref:`rendering-lyrics`. - **sources**: List of sources to search for lyrics. An asterisk ``*`` expands to all available sources. The ``google`` source will be automatically deactivated if no ``google_API_key`` is setup. By default, ``musixmatch`` and @@ -147,6 +150,8 @@ lyrics without touching tracks that already have a synced version. Inversely, the ``-l, --local`` option restricts operations to lyrics that are locally available, which show lyrics faster without using the network at all. +.. _rendering-lyrics: + Rendering Lyrics into Other Formats ----------------------------------- diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py index 220d57a45d..36c128d2ce 100644 --- a/test/plugins/test_lyrics.py +++ b/test/plugins/test_lyrics.py @@ -20,6 +20,7 @@ import textwrap from functools import partial from http import HTTPStatus +from pathlib import Path from types import SimpleNamespace from typing import TYPE_CHECKING @@ -27,15 +28,13 @@ import requests from beets.library import Item -from beets.test.helper import PluginMixin +from beets.test.helper import PluginMixin, PluginTestHelper from beets.util.lyrics import Lyrics from beetsplug import lyrics from .lyrics_pages import lyrics_pages if TYPE_CHECKING: - from pathlib import Path - from .lyrics_pages import LyricsPage PHRASE_BY_TITLE = { @@ -843,6 +842,91 @@ def test_write(self, rest_dir: Path, rest_files): ) +class TestLyricsRestDirectory(PluginTestHelper): + plugin = "lyrics" + + @pytest.fixture + def lib(self, helper): + return helper.lib + + @pytest.mark.parametrize( + "config_path, arg_path, output_path", + [ + pytest.param( + "test/config", + "test/cmd", + "test/cmd", + id="config and cmd arg, relative path", + ), + pytest.param( + "test/config", + None, + "test/config", + id="config only, relative path", + ), + pytest.param( + None, "test/cmd", "test/cmd", id="cmd arg only, relative path" + ), + pytest.param( + "/test/config", + "/test/cmd", + "/test/cmd", + id="config and cmd arg, absolute path", + ), + pytest.param( + "/test/config", + None, + "/test/config", + id="config only, absolute path", + ), + pytest.param( + None, "/test/cmd", "/test/cmd", id="cmd arg only, absolute path" + ), + pytest.param( + "~/test/config", + "~/test/cmd", + "~/test/cmd", + id="config and cmd arg, home path", + ), + pytest.param( + "~/test/config", + None, + "~/test/config", + id="config only, home path", + ), + pytest.param( + None, "~/test/cmd", "~/test/cmd", id="cmd arg only, home path" + ), + pytest.param(None, None, None, id="no output"), + ], + ) + def test_rest_config( + self, monkeypatch, lib, config_path, arg_path, output_path + ): + test_capture = {} + + class MockRestFiles: + def __init__(self, directory): + test_capture["directory"] = directory + + def write(self, items): + test_capture["items"] = items + + monkeypatch.setattr(lyrics, "RestFiles", MockRestFiles) + + if config_path: + self.config["lyrics"]["rest_directory"] = config_path + + cmd_args = [] if arg_path is None else ["-r", arg_path] + self.run_command("lyrics", *cmd_args, lib=lib) + + test_output = test_capture.get("directory") + if output_path is None: + assert test_output is None + else: + assert test_output == Path(output_path).expanduser() + + class TestLyricsSyltProperty: """Unit tests for the Lyrics.sylt timestamp-to-millisecond converter."""