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
@@ -865,6 +831,97 @@ def translate(self, lyrics: str) -> str:
865
831
return "\n \n Source: " .join (["\n " .join (translated_lines ), * url ])
866
832
867
833
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
+
868
925
class LyricsPlugin (RequestHandler , plugins .BeetsPlugin ):
869
926
BACKEND_BY_NAME = {
870
927
b .name : b for b in [LRCLib , Google , Genius , Tekstowo , MusiXmatch ]
@@ -922,15 +979,6 @@ def __init__(self):
922
979
self .config ["google_engine_ID" ].redact = True
923
980
self .config ["genius_api_key" ].redact = True
924
981
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
-
934
982
def commands (self ):
935
983
cmd = ui .Subcommand ("lyrics" , help = "fetch song lyrics" )
936
984
cmd .parser .add_option (
@@ -944,7 +992,7 @@ def commands(self):
944
992
cmd .parser .add_option (
945
993
"-r" ,
946
994
"--write-rest" ,
947
- dest = "writerest " ,
995
+ dest = "rest_directory " ,
948
996
action = "store" ,
949
997
default = None ,
950
998
metavar = "dir" ,
@@ -970,99 +1018,26 @@ def commands(self):
970
1018
def func (lib , opts , args ):
971
1019
# The "write to files" option corresponds to the
972
1020
# 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 )))
977
1022
for item in items :
978
1023
if not opts .local_only and not self .config ["local" ]:
979
1024
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" ],
981
1028
)
982
1029
if item .lyrics :
983
1030
if opts .printlyr :
984
1031
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 )
1004
1037
1005
1038
cmd .func = func
1006
1039
return [cmd ]
1007
1040
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
-
1066
1041
def imported (self , _ , task : ImportTask ) -> None :
1067
1042
"""Import hook for fetching lyrics automatically."""
1068
1043
if self .config ["auto" ]:
0 commit comments