Skip to content

Commit c95156a

Browse files
committed
Refactor writing rest files
1 parent d720106 commit c95156a

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, Iterable, Iterator, NamedTuple
3132
from urllib.parse import quote, quote_plus, urlencode, urlparse
3233

@@ -56,41 +57,6 @@
5657
USER_AGENT = f"beets/{beets.__version__}"
5758
INSTRUMENTAL_LYRICS = "[Instrumental]"
5859

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

9561
class NotFoundError(requests.exceptions.HTTPError):
9662
pass
@@ -865,6 +831,97 @@ def translate(self, lyrics: str) -> str:
865831
return "\n\nSource: ".join(["\n".join(translated_lines), *url])
866832

867833

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

925-
# State information for the ReST writer.
926-
# First, the current artist we're writing.
927-
self.artist = "Unknown artist"
928-
# The current album: False means no album yet.
929-
self.album = False
930-
# The current rest file content. None means the file is not
931-
# open yet.
932-
self.rest = None
933-
934982
def commands(self):
935983
cmd = ui.Subcommand("lyrics", help="fetch song lyrics")
936984
cmd.parser.add_option(
@@ -944,7 +992,7 @@ def commands(self):
944992
cmd.parser.add_option(
945993
"-r",
946994
"--write-rest",
947-
dest="writerest",
995+
dest="rest_directory",
948996
action="store",
949997
default=None,
950998
metavar="dir",
@@ -970,99 +1018,26 @@ def commands(self):
9701018
def func(lib, opts, args):
9711019
# The "write to files" option corresponds to the
9721020
# import_write config value.
973-
write = ui.should_write()
974-
if opts.writerest:
975-
self.writerest_indexes(opts.writerest)
976-
items = lib.items(ui.decargs(args))
1021+
items = list(lib.items(ui.decargs(args)))
9771022
for item in items:
9781023
if not opts.local_only and not self.config["local"]:
9791024
self.fetch_item_lyrics(
980-
item, write, opts.force_refetch or self.config["force"]
1025+
item,
1026+
ui.should_write(),
1027+
opts.force_refetch or self.config["force"],
9811028
)
9821029
if item.lyrics:
9831030
if opts.printlyr:
9841031
ui.print_(item.lyrics)
985-
if opts.writerest:
986-
self.appendrest(opts.writerest, item)
987-
if opts.writerest and items:
988-
# flush last artist & write to ReST
989-
self.writerest(opts.writerest)
990-
ui.print_("ReST files generated. to build, use one of:")
991-
ui.print_(
992-
" sphinx-build -b html %s _build/html" % opts.writerest
993-
)
994-
ui.print_(
995-
" sphinx-build -b epub %s _build/epub" % opts.writerest
996-
)
997-
ui.print_(
998-
(
999-
" sphinx-build -b latex %s _build/latex "
1000-
"&& make -C _build/latex all-pdf"
1001-
)
1002-
% opts.writerest
1003-
)
1032+
1033+
if opts.rest_directory and (
1034+
items := [i for i in items if i.lyrics]
1035+
):
1036+
RestFiles(Path(opts.rest_directory)).write(items)
10041037

10051038
cmd.func = func
10061039
return [cmd]
10071040

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

docs/plugins/lyrics.rst

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

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

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

123122
::
124123

125-
sphinx-build -b html . _build/html
124+
sphinx-build -b html <dir> <dir>/html
126125

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

129128
::
130129

131-
sphinx-build -b epub3 . _build/epub
130+
sphinx-build -b epub3 <dir> <dir>/epub
132131

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

135134
::
136135

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

139138

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

test/plugins/test_lyrics.py

+49
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import textwrap
2121
from functools import partial
2222
from http import HTTPStatus
23+
from pathlib import Path
2324

2425
import pytest
2526

@@ -594,3 +595,51 @@ def test_translate(self, initial_lyrics, expected):
594595
assert bing.translate(
595596
textwrap.dedent(initial_lyrics)
596597
) == textwrap.dedent(expected)
598+
599+
600+
class TestRestFiles:
601+
@pytest.fixture
602+
def rest_dir(self, tmp_path):
603+
return tmp_path
604+
605+
@pytest.fixture
606+
def rest_files(self, rest_dir):
607+
return lyrics.RestFiles(rest_dir)
608+
609+
def test_write(self, rest_dir: Path, rest_files):
610+
items = [
611+
Item(albumartist=aa, album=a, title=t, lyrics=lyr)
612+
for aa, a, t, lyr in [
613+
("Artist One", "Album One", "Song One", "Lyrics One"),
614+
("Artist One", "Album One", "Song Two", "Lyrics Two"),
615+
("Artist Two", "Album Two", "Song Three", "Lyrics Three"),
616+
]
617+
]
618+
619+
rest_files.write(items)
620+
621+
assert (rest_dir / "index.rst").exists()
622+
assert (rest_dir / "conf.py").exists()
623+
624+
artist_one_file = rest_dir / "artists" / "artist-one.rst"
625+
artist_two_file = rest_dir / "artists" / "artist-two.rst"
626+
assert artist_one_file.exists()
627+
assert artist_two_file.exists()
628+
629+
c = artist_one_file.read_text()
630+
assert (
631+
c.index("Artist One")
632+
< c.index("Album One")
633+
< c.index("Song One")
634+
< c.index("Lyrics One")
635+
< c.index("Song Two")
636+
< c.index("Lyrics Two")
637+
)
638+
639+
c = artist_two_file.read_text()
640+
assert (
641+
c.index("Artist Two")
642+
< c.index("Album Two")
643+
< c.index("Song Three")
644+
< c.index("Lyrics Three")
645+
)

0 commit comments

Comments
 (0)