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 , ClassVar , Iterable , Iterator , NamedTuple
31
32
from urllib .parse import quote , urlparse
32
33
53
54
USER_AGENT = f"beets/{ beets .__version__ } "
54
55
INSTRUMENTAL_LYRICS = "[Instrumental]"
55
56
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
-
91
57
92
58
class NotFoundError (requests .exceptions .HTTPError ):
93
59
pass
@@ -854,6 +820,97 @@ def translate(self, lyrics: str) -> str:
854
820
return "\n \n Source: " .join (["\n " .join (translated_lines ), * url ])
855
821
856
822
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
+
857
914
class LyricsPlugin (RequestHandler , plugins .BeetsPlugin ):
858
915
BACKEND_BY_NAME = {
859
916
b .name : b for b in [LRCLib , Google , Genius , Tekstowo , MusiXmatch ]
@@ -911,15 +968,6 @@ def __init__(self):
911
968
self .config ["google_engine_ID" ].redact = True
912
969
self .config ["genius_api_key" ].redact = True
913
970
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
-
923
971
def commands (self ):
924
972
cmd = ui .Subcommand ("lyrics" , help = "fetch song lyrics" )
925
973
cmd .parser .add_option (
@@ -933,7 +981,7 @@ def commands(self):
933
981
cmd .parser .add_option (
934
982
"-r" ,
935
983
"--write-rest" ,
936
- dest = "writerest " ,
984
+ dest = "rest_directory " ,
937
985
action = "store" ,
938
986
default = None ,
939
987
metavar = "dir" ,
@@ -959,99 +1007,26 @@ def commands(self):
959
1007
def func (lib , opts , args ):
960
1008
# The "write to files" option corresponds to the
961
1009
# 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 )))
966
1011
for item in items :
967
1012
if not opts .local_only and not self .config ["local" ]:
968
1013
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" ],
970
1017
)
971
1018
if item .lyrics :
972
1019
if opts .printlyr :
973
1020
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 )
993
1026
994
1027
cmd .func = func
995
1028
return [cmd ]
996
1029
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
-
1055
1030
def imported (self , _ , task : ImportTask ) -> None :
1056
1031
"""Import hook for fetching lyrics automatically."""
1057
1032
if self .config ["auto" ]:
0 commit comments