17
17
from __future__ import annotations
18
18
19
19
import atexit
20
- import errno
21
20
import itertools
22
21
import math
23
- import os .path
24
22
import re
23
+ import textwrap
25
24
from contextlib import contextmanager , suppress
26
25
from dataclasses import dataclass
27
26
from functools import cached_property , partial , total_ordering
28
27
from html import unescape
29
28
from http import HTTPStatus
29
+ from itertools import groupby
30
+ from pathlib import Path
30
31
from typing import TYPE_CHECKING , Iterable , Iterator , NamedTuple
31
32
from urllib .parse import quote , quote_plus , urlencode , urlparse
32
33
56
57
USER_AGENT = f"beets/{ beets .__version__ } "
57
58
INSTRUMENTAL_LYRICS = "[Instrumental]"
58
59
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
-
94
60
95
61
class NotFoundError (requests .exceptions .HTTPError ):
96
62
pass
@@ -868,6 +834,97 @@ def translate(self, lyrics: str) -> str:
868
834
return "\n \n Source: " .join (["\n " .join (translated_lines ), * url ])
869
835
870
836
837
+ @dataclass
838
+ class RestFiles :
839
+ # The content for the base index.rst generated in ReST mode.
840
+ REST_INDEX_TEMPLATE = textwrap .dedent ("""
841
+ Lyrics
842
+ ======
843
+
844
+ * :ref:`Song index <genindex>`
845
+ * :ref:`search`
846
+
847
+ Artist index:
848
+
849
+ .. toctree::
850
+ :maxdepth: 1
851
+ :glob:
852
+
853
+ artists/*
854
+ """ ).strip ()
855
+
856
+ # The content for the base conf.py generated.
857
+ REST_CONF_TEMPLATE = textwrap .dedent ("""
858
+ master_doc = "index"
859
+ project = "Lyrics"
860
+ copyright = "none"
861
+ author = "Various Authors"
862
+ latex_documents = [
863
+ (master_doc, "Lyrics.tex", project, author, "manual"),
864
+ ]
865
+ epub_exclude_files = ["search.html"]
866
+ epub_tocdepth = 1
867
+ epub_tocdup = False
868
+ """ ).strip ()
869
+
870
+ directory : Path
871
+
872
+ @cached_property
873
+ def artists_dir (self ) -> Path :
874
+ dir = self .directory / "artists"
875
+ dir .mkdir (parents = True , exist_ok = True )
876
+ return dir
877
+
878
+ def write_indexes (self ) -> None :
879
+ """Write conf.py and index.rst files necessary for Sphinx
880
+
881
+ We write minimal configurations that are necessary for Sphinx
882
+ to operate. We do not overwrite existing files so that
883
+ customizations are respected."""
884
+ index_file = self .directory / "index.rst"
885
+ if not index_file .exists ():
886
+ index_file .write_text (self .REST_INDEX_TEMPLATE )
887
+ conf_file = self .directory / "conf.py"
888
+ if not conf_file .exists ():
889
+ conf_file .write_text (self .REST_CONF_TEMPLATE )
890
+
891
+ def write_artist (self , artist : str , items : Iterable [Item ]) -> None :
892
+ parts = [
893
+ f'{ artist } \n { "=" * len (artist )} ' ,
894
+ ".. contents::\n :local:" ,
895
+ ]
896
+ for album , items in groupby (items , key = lambda i : i .album ):
897
+ parts .append (f'{ album } \n { "-" * len (album )} ' )
898
+ parts .extend (
899
+ part
900
+ for i in items
901
+ if (title := f":index:`{ i .title .strip ()} `" )
902
+ for part in (
903
+ f'{ title } \n { "~" * len (title )} ' ,
904
+ textwrap .indent (i .lyrics , "| " ),
905
+ )
906
+ )
907
+ file = self .artists_dir / f"{ slug (artist )} .rst"
908
+ file .write_text ("\n \n " .join (parts ).strip ())
909
+
910
+ def write (self , items : list [Item ]) -> None :
911
+ self .directory .mkdir (exist_ok = True , parents = True )
912
+ self .write_indexes ()
913
+
914
+ items .sort (key = lambda i : i .albumartist )
915
+ for artist , artist_items in groupby (items , key = lambda i : i .albumartist ):
916
+ self .write_artist (artist .strip (), artist_items )
917
+
918
+ d = self .directory
919
+ text = f"""
920
+ ReST files generated. to build, use one of:
921
+ sphinx-build -b html { d } { d / "html" }
922
+ sphinx-build -b epub { d } { d / "epub" }
923
+ sphinx-build -b latex { d } { d / "latex" } && make -C { d / "latex" } all-pdf
924
+ """
925
+ ui .print_ (textwrap .dedent (text ))
926
+
927
+
871
928
class LyricsPlugin (RequestHandler , plugins .BeetsPlugin ):
872
929
BACKEND_BY_NAME = {
873
930
b .name : b for b in [LRCLib , Google , Genius , Tekstowo , MusiXmatch ]
@@ -925,15 +982,6 @@ def __init__(self):
925
982
self .config ["google_engine_ID" ].redact = True
926
983
self .config ["genius_api_key" ].redact = True
927
984
928
- # State information for the ReST writer.
929
- # First, the current artist we're writing.
930
- self .artist = "Unknown artist"
931
- # The current album: False means no album yet.
932
- self .album = False
933
- # The current rest file content. None means the file is not
934
- # open yet.
935
- self .rest = None
936
-
937
985
def commands (self ):
938
986
cmd = ui .Subcommand ("lyrics" , help = "fetch song lyrics" )
939
987
cmd .parser .add_option (
@@ -947,7 +995,7 @@ def commands(self):
947
995
cmd .parser .add_option (
948
996
"-r" ,
949
997
"--write-rest" ,
950
- dest = "writerest " ,
998
+ dest = "rest_directory " ,
951
999
action = "store" ,
952
1000
default = None ,
953
1001
metavar = "dir" ,
@@ -973,99 +1021,26 @@ def commands(self):
973
1021
def func (lib , opts , args ):
974
1022
# The "write to files" option corresponds to the
975
1023
# import_write config value.
976
- write = ui .should_write ()
977
- if opts .writerest :
978
- self .writerest_indexes (opts .writerest )
979
- items = lib .items (ui .decargs (args ))
1024
+ items = list (lib .items (ui .decargs (args )))
980
1025
for item in items :
981
1026
if not opts .local_only and not self .config ["local" ]:
982
1027
self .fetch_item_lyrics (
983
- item , write , opts .force_refetch or self .config ["force" ]
1028
+ item ,
1029
+ ui .should_write (),
1030
+ opts .force_refetch or self .config ["force" ],
984
1031
)
985
1032
if item .lyrics :
986
1033
if opts .printlyr :
987
1034
ui .print_ (item .lyrics )
988
- if opts .writerest :
989
- self .appendrest (opts .writerest , item )
990
- if opts .writerest and items :
991
- # flush last artist & write to ReST
992
- self .writerest (opts .writerest )
993
- ui .print_ ("ReST files generated. to build, use one of:" )
994
- ui .print_ (
995
- " sphinx-build -b html %s _build/html" % opts .writerest
996
- )
997
- ui .print_ (
998
- " sphinx-build -b epub %s _build/epub" % opts .writerest
999
- )
1000
- ui .print_ (
1001
- (
1002
- " sphinx-build -b latex %s _build/latex "
1003
- "&& make -C _build/latex all-pdf"
1004
- )
1005
- % opts .writerest
1006
- )
1035
+
1036
+ if opts .rest_directory and (
1037
+ items := [i for i in items if i .lyrics ]
1038
+ ):
1039
+ RestFiles (Path (opts .rest_directory )).write (items )
1007
1040
1008
1041
cmd .func = func
1009
1042
return [cmd ]
1010
1043
1011
- def appendrest (self , directory , item ):
1012
- """Append the item to an ReST file
1013
-
1014
- This will keep state (in the `rest` variable) in order to avoid
1015
- writing continuously to the same files.
1016
- """
1017
-
1018
- if slug (self .artist ) != slug (item .albumartist ):
1019
- # Write current file and start a new one ~ item.albumartist
1020
- self .writerest (directory )
1021
- self .artist = item .albumartist .strip ()
1022
- self .rest = "%s\n %s\n \n .. contents::\n :local:\n \n " % (
1023
- self .artist ,
1024
- "=" * len (self .artist ),
1025
- )
1026
-
1027
- if self .album != item .album :
1028
- tmpalbum = self .album = item .album .strip ()
1029
- if self .album == "" :
1030
- tmpalbum = "Unknown album"
1031
- self .rest += "{}\n {}\n \n " .format (tmpalbum , "-" * len (tmpalbum ))
1032
- title_str = ":index:`%s`" % item .title .strip ()
1033
- block = "| " + item .lyrics .replace ("\n " , "\n | " )
1034
- self .rest += "{}\n {}\n \n {}\n \n " .format (
1035
- title_str , "~" * len (title_str ), block
1036
- )
1037
-
1038
- def writerest (self , directory ):
1039
- """Write self.rest to a ReST file"""
1040
- if self .rest is not None and self .artist is not None :
1041
- path = os .path .join (
1042
- directory , "artists" , slug (self .artist ) + ".rst"
1043
- )
1044
- with open (path , "wb" ) as output :
1045
- output .write (self .rest .encode ("utf-8" ))
1046
-
1047
- def writerest_indexes (self , directory ):
1048
- """Write conf.py and index.rst files necessary for Sphinx
1049
-
1050
- We write minimal configurations that are necessary for Sphinx
1051
- to operate. We do not overwrite existing files so that
1052
- customizations are respected."""
1053
- try :
1054
- os .makedirs (os .path .join (directory , "artists" ))
1055
- except OSError as e :
1056
- if e .errno == errno .EEXIST :
1057
- pass
1058
- else :
1059
- raise
1060
- indexfile = os .path .join (directory , "index.rst" )
1061
- if not os .path .exists (indexfile ):
1062
- with open (indexfile , "w" ) as output :
1063
- output .write (REST_INDEX_TEMPLATE )
1064
- conffile = os .path .join (directory , "conf.py" )
1065
- if not os .path .exists (conffile ):
1066
- with open (conffile , "w" ) as output :
1067
- output .write (REST_CONF_TEMPLATE )
1068
-
1069
1044
def imported (self , _ , task : ImportTask ) -> None :
1070
1045
"""Import hook for fetching lyrics automatically."""
1071
1046
if self .config ["auto" ]:
0 commit comments