Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fa42b09

Browse files
committedJan 12, 2025
Refactor writing rest files
1 parent ef105a9 commit fa42b09

File tree

3 files changed

+158
-135
lines changed

3 files changed

+158
-135
lines changed
 

‎beetsplug/lyrics.py

+104-129
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@
1717
from __future__ import annotations
1818

1919
import atexit
20-
import errno
2120
import itertools
2221
import math
23-
import os.path
2422
import re
23+
import textwrap
2524
from contextlib import contextmanager, suppress
2625
from dataclasses import dataclass
2726
from functools import cached_property, partial, total_ordering
2827
from html import unescape
2928
from http import HTTPStatus
29+
from itertools import groupby
30+
from pathlib import Path
3031
from typing import TYPE_CHECKING, ClassVar, Iterable, Iterator, NamedTuple
3132
from urllib.parse import quote, urlparse
3233

@@ -53,41 +54,6 @@
5354
USER_AGENT = f"beets/{beets.__version__}"
5455
INSTRUMENTAL_LYRICS = "[Instrumental]"
5556

56-
# The content for the base index.rst generated in ReST mode.
57-
REST_INDEX_TEMPLATE = """Lyrics
58-
======
59-
60-
* :ref:`Song index <genindex>`
61-
* :ref:`search`
62-
63-
Artist index:
64-
65-
.. toctree::
66-
:maxdepth: 1
67-
:glob:
68-
69-
artists/*
70-
"""
71-
72-
# The content for the base conf.py generated.
73-
REST_CONF_TEMPLATE = """# -*- coding: utf-8 -*-
74-
master_doc = 'index'
75-
project = 'Lyrics'
76-
copyright = 'none'
77-
author = 'Various Authors'
78-
latex_documents = [
79-
(master_doc, 'Lyrics.tex', project,
80-
author, 'manual'),
81-
]
82-
epub_title = project
83-
epub_author = author
84-
epub_publisher = author
85-
epub_copyright = copyright
86-
epub_exclude_files = ['search.html']
87-
epub_tocdepth = 1
88-
epub_tocdup = False
89-
"""
90-
9157

9258
class NotFoundError(requests.exceptions.HTTPError):
9359
pass
@@ -854,6 +820,97 @@ def translate(self, lyrics: str) -> str:
854820
return "\n\nSource: ".join(["\n".join(translated_lines), *url])
855821

856822

823+
@dataclass
824+
class RestFiles:
825+
# The content for the base index.rst generated in ReST mode.
826+
REST_INDEX_TEMPLATE = textwrap.dedent("""
827+
Lyrics
828+
======
829+
830+
* :ref:`Song index <genindex>`
831+
* :ref:`search`
832+
833+
Artist index:
834+
835+
.. toctree::
836+
:maxdepth: 1
837+
:glob:
838+
839+
artists/*
840+
""").strip()
841+
842+
# The content for the base conf.py generated.
843+
REST_CONF_TEMPLATE = textwrap.dedent("""
844+
master_doc = "index"
845+
project = "Lyrics"
846+
copyright = "none"
847+
author = "Various Authors"
848+
latex_documents = [
849+
(master_doc, "Lyrics.tex", project, author, "manual"),
850+
]
851+
epub_exclude_files = ["search.html"]
852+
epub_tocdepth = 1
853+
epub_tocdup = False
854+
""").strip()
855+
856+
directory: Path
857+
858+
@cached_property
859+
def artists_dir(self) -> Path:
860+
dir = self.directory / "artists"
861+
dir.mkdir(parents=True, exist_ok=True)
862+
return dir
863+
864+
def write_indexes(self) -> None:
865+
"""Write conf.py and index.rst files necessary for Sphinx
866+
867+
We write minimal configurations that are necessary for Sphinx
868+
to operate. We do not overwrite existing files so that
869+
customizations are respected."""
870+
index_file = self.directory / "index.rst"
871+
if not index_file.exists():
872+
index_file.write_text(self.REST_INDEX_TEMPLATE)
873+
conf_file = self.directory / "conf.py"
874+
if not conf_file.exists():
875+
conf_file.write_text(self.REST_CONF_TEMPLATE)
876+
877+
def write_artist(self, artist: str, items: Iterable[Item]) -> None:
878+
parts = [
879+
f'{artist}\n{"=" * len(artist)}',
880+
".. contents::\n :local:",
881+
]
882+
for album, items in groupby(items, key=lambda i: i.album):
883+
parts.append(f'{album}\n{"-" * len(album)}')
884+
parts.extend(
885+
part
886+
for i in items
887+
if (title := f":index:`{i.title.strip()}`")
888+
for part in (
889+
f'{title}\n{"~" * len(title)}',
890+
textwrap.indent(i.lyrics, "| "),
891+
)
892+
)
893+
file = self.artists_dir / f"{slug(artist)}.rst"
894+
file.write_text("\n\n".join(parts).strip())
895+
896+
def write(self, items: list[Item]) -> None:
897+
self.directory.mkdir(exist_ok=True, parents=True)
898+
self.write_indexes()
899+
900+
items.sort(key=lambda i: i.albumartist)
901+
for artist, artist_items in groupby(items, key=lambda i: i.albumartist):
902+
self.write_artist(artist.strip(), artist_items)
903+
904+
d = self.directory
905+
text = f"""
906+
ReST files generated. to build, use one of:
907+
sphinx-build -b html {d} {d/"html"}
908+
sphinx-build -b epub {d} {d/"epub"}
909+
sphinx-build -b latex {d} {d/"latex"} && make -C {d/"latex"} all-pdf
910+
"""
911+
ui.print_(textwrap.dedent(text))
912+
913+
857914
class LyricsPlugin(RequestHandler, plugins.BeetsPlugin):
858915
BACKEND_BY_NAME = {
859916
b.name: b for b in [LRCLib, Google, Genius, Tekstowo, MusiXmatch]
@@ -911,15 +968,6 @@ def __init__(self):
911968
self.config["google_engine_ID"].redact = True
912969
self.config["genius_api_key"].redact = True
913970

914-
# State information for the ReST writer.
915-
# First, the current artist we're writing.
916-
self.artist = "Unknown artist"
917-
# The current album: False means no album yet.
918-
self.album = False
919-
# The current rest file content. None means the file is not
920-
# open yet.
921-
self.rest = None
922-
923971
def commands(self):
924972
cmd = ui.Subcommand("lyrics", help="fetch song lyrics")
925973
cmd.parser.add_option(
@@ -933,7 +981,7 @@ def commands(self):
933981
cmd.parser.add_option(
934982
"-r",
935983
"--write-rest",
936-
dest="writerest",
984+
dest="rest_directory",
937985
action="store",
938986
default=None,
939987
metavar="dir",
@@ -959,99 +1007,26 @@ def commands(self):
9591007
def func(lib, opts, args):
9601008
# The "write to files" option corresponds to the
9611009
# import_write config value.
962-
write = ui.should_write()
963-
if opts.writerest:
964-
self.writerest_indexes(opts.writerest)
965-
items = lib.items(ui.decargs(args))
1010+
items = list(lib.items(ui.decargs(args)))
9661011
for item in items:
9671012
if not opts.local_only and not self.config["local"]:
9681013
self.fetch_item_lyrics(
969-
item, write, opts.force_refetch or self.config["force"]
1014+
item,
1015+
ui.should_write(),
1016+
opts.force_refetch or self.config["force"],
9701017
)
9711018
if item.lyrics:
9721019
if opts.printlyr:
9731020
ui.print_(item.lyrics)
974-
if opts.writerest:
975-
self.appendrest(opts.writerest, item)
976-
if opts.writerest and items:
977-
# flush last artist & write to ReST
978-
self.writerest(opts.writerest)
979-
ui.print_("ReST files generated. to build, use one of:")
980-
ui.print_(
981-
" sphinx-build -b html %s _build/html" % opts.writerest
982-
)
983-
ui.print_(
984-
" sphinx-build -b epub %s _build/epub" % opts.writerest
985-
)
986-
ui.print_(
987-
(
988-
" sphinx-build -b latex %s _build/latex "
989-
"&& make -C _build/latex all-pdf"
990-
)
991-
% opts.writerest
992-
)
1021+
1022+
if opts.rest_directory and (
1023+
items := [i for i in items if i.lyrics]
1024+
):
1025+
RestFiles(Path(opts.rest_directory)).write(items)
9931026

9941027
cmd.func = func
9951028
return [cmd]
9961029

997-
def appendrest(self, directory, item):
998-
"""Append the item to an ReST file
999-
1000-
This will keep state (in the `rest` variable) in order to avoid
1001-
writing continuously to the same files.
1002-
"""
1003-
1004-
if slug(self.artist) != slug(item.albumartist):
1005-
# Write current file and start a new one ~ item.albumartist
1006-
self.writerest(directory)
1007-
self.artist = item.albumartist.strip()
1008-
self.rest = "%s\n%s\n\n.. contents::\n :local:\n\n" % (
1009-
self.artist,
1010-
"=" * len(self.artist),
1011-
)
1012-
1013-
if self.album != item.album:
1014-
tmpalbum = self.album = item.album.strip()
1015-
if self.album == "":
1016-
tmpalbum = "Unknown album"
1017-
self.rest += "{}\n{}\n\n".format(tmpalbum, "-" * len(tmpalbum))
1018-
title_str = ":index:`%s`" % item.title.strip()
1019-
block = "| " + item.lyrics.replace("\n", "\n| ")
1020-
self.rest += "{}\n{}\n\n{}\n\n".format(
1021-
title_str, "~" * len(title_str), block
1022-
)
1023-
1024-
def writerest(self, directory):
1025-
"""Write self.rest to a ReST file"""
1026-
if self.rest is not None and self.artist is not None:
1027-
path = os.path.join(
1028-
directory, "artists", slug(self.artist) + ".rst"
1029-
)
1030-
with open(path, "wb") as output:
1031-
output.write(self.rest.encode("utf-8"))
1032-
1033-
def writerest_indexes(self, directory):
1034-
"""Write conf.py and index.rst files necessary for Sphinx
1035-
1036-
We write minimal configurations that are necessary for Sphinx
1037-
to operate. We do not overwrite existing files so that
1038-
customizations are respected."""
1039-
try:
1040-
os.makedirs(os.path.join(directory, "artists"))
1041-
except OSError as e:
1042-
if e.errno == errno.EEXIST:
1043-
pass
1044-
else:
1045-
raise
1046-
indexfile = os.path.join(directory, "index.rst")
1047-
if not os.path.exists(indexfile):
1048-
with open(indexfile, "w") as output:
1049-
output.write(REST_INDEX_TEMPLATE)
1050-
conffile = os.path.join(directory, "conf.py")
1051-
if not os.path.exists(conffile):
1052-
with open(conffile, "w") as output:
1053-
output.write(REST_CONF_TEMPLATE)
1054-
10551030
def imported(self, _, task: ImportTask) -> None:
10561031
"""Import hook for fetching lyrics automatically."""
10571032
if self.config["auto"]:

‎docs/plugins/lyrics.rst

+5-6
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,8 @@ Rendering Lyrics into Other Formats
106106
-----------------------------------
107107

108108
The ``-r directory, --write-rest directory`` option renders all lyrics as
109-
`reStructuredText`_ (ReST) documents in ``directory`` (by default, the current
110-
directory). That directory, in turn, can be parsed by tools like `Sphinx`_ to
111-
generate HTML, ePUB, or PDF documents.
109+
`reStructuredText`_ (ReST) documents in ``directory``. That directory, in turn,
110+
can be parsed by tools like `Sphinx`_ to generate HTML, ePUB, or PDF documents.
112111

113112
Minimal ``conf.py`` and ``index.rst`` files are created the first time the
114113
command is run. They are not overwritten on subsequent runs, so you can safely
@@ -121,19 +120,19 @@ Sphinx supports various `builders`_, see a few suggestions:
121120

122121
::
123122

124-
sphinx-build -b html . _build/html
123+
sphinx-build -b html <dir> <dir>/html
125124

126125
.. admonition:: Build an ePUB3 formatted file, usable on ebook readers
127126

128127
::
129128

130-
sphinx-build -b epub3 . _build/epub
129+
sphinx-build -b epub3 <dir> <dir>/epub
131130

132131
.. admonition:: Build a PDF file, which incidentally also builds a LaTeX file
133132

134133
::
135134

136-
sphinx-build -b latex %s _build/latex && make -C _build/latex all-pdf
135+
sphinx-build -b latex <dir> <dir>/latex && make -C <dir>/latex all-pdf
137136

138137

139138
.. _Sphinx: https://www.sphinx-doc.org/

‎test/plugins/test_lyrics.py

+49
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import textwrap
1919
from functools import partial
2020
from http import HTTPStatus
21+
from pathlib import Path
2122

2223
import pytest
2324

@@ -571,3 +572,51 @@ def test_translate(self, initial_lyrics, expected):
571572
assert bing.translate(
572573
textwrap.dedent(initial_lyrics)
573574
) == textwrap.dedent(expected)
575+
576+
577+
class TestRestFiles:
578+
@pytest.fixture
579+
def rest_dir(self, tmp_path):
580+
return tmp_path
581+
582+
@pytest.fixture
583+
def rest_files(self, rest_dir):
584+
return lyrics.RestFiles(rest_dir)
585+
586+
def test_write(self, rest_dir: Path, rest_files):
587+
items = [
588+
Item(albumartist=aa, album=a, title=t, lyrics=lyr)
589+
for aa, a, t, lyr in [
590+
("Artist One", "Album One", "Song One", "Lyrics One"),
591+
("Artist One", "Album One", "Song Two", "Lyrics Two"),
592+
("Artist Two", "Album Two", "Song Three", "Lyrics Three"),
593+
]
594+
]
595+
596+
rest_files.write(items)
597+
598+
assert (rest_dir / "index.rst").exists()
599+
assert (rest_dir / "conf.py").exists()
600+
601+
artist_one_file = rest_dir / "artists" / "artist-one.rst"
602+
artist_two_file = rest_dir / "artists" / "artist-two.rst"
603+
assert artist_one_file.exists()
604+
assert artist_two_file.exists()
605+
606+
c = artist_one_file.read_text()
607+
assert (
608+
c.index("Artist One")
609+
< c.index("Album One")
610+
< c.index("Song One")
611+
< c.index("Lyrics One")
612+
< c.index("Song Two")
613+
< c.index("Lyrics Two")
614+
)
615+
616+
c = artist_two_file.read_text()
617+
assert (
618+
c.index("Artist Two")
619+
< c.index("Album Two")
620+
< c.index("Song Three")
621+
< c.index("Lyrics Three")
622+
)

0 commit comments

Comments
 (0)
Please sign in to comment.