generate_slides.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. # Copyright © 2024 Noah Vogt <noah@noahvogt.com>
  2. # This program is free software: you can redistribute it and/or modify
  3. # it under the terms of the GNU General Public License as published by
  4. # the Free Software Foundation, either version 3 of the License, or
  5. # (at your option) any later version.
  6. # This program is distributed in the hope that it will be useful,
  7. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  8. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  9. # GNU General Public License for more details.
  10. # You should have received a copy of the GNU General Public License
  11. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  12. from threading import Thread
  13. from os import path, utime
  14. from pathlib import Path
  15. from re import compile
  16. import datetime
  17. from wand.exceptions import BlobError
  18. from utils import (
  19. log,
  20. error_msg,
  21. )
  22. import config as const
  23. # used in attempt to get a correct ordering as obs image slide shows ignores
  24. # filenames, but it appearently also ignores a/m-times as seen in
  25. # https://github.com/obsproject/obs-studio/issues/10382
  26. def fix_timestamps(slidegen):
  27. log("fixing timestamps...")
  28. folder = Path(slidegen.output_dir).resolve()
  29. pattern = compile(rf"{const.FILE_NAMING}(\d+)\.jpg$")
  30. slides = []
  31. for f in folder.iterdir():
  32. m = pattern.match(f.name)
  33. if m:
  34. num = int(m.group(1))
  35. slides.append((num, f))
  36. slides.sort(key=lambda x: x[0])
  37. base_date = datetime.date(2000, 1, 1)
  38. for i, (num, file_path) in enumerate(slides, start=1):
  39. new_date = base_date + datetime.timedelta(days=i - 1)
  40. new_dt = datetime.datetime.combine(new_date, datetime.time(12, 0, 0))
  41. ts = new_dt.timestamp()
  42. utime(file_path, (ts, ts))
  43. def generate_slides(
  44. slidegen, slide_count, template_img, zfill_length, disable_async: bool
  45. ) -> list[Thread]:
  46. log("generating song slides...")
  47. current_slide_index: int = 0
  48. log("spawning subprocess for start slide...", color="yellow")
  49. threads = [
  50. Thread(
  51. target=generate_start_slide,
  52. args=(slidegen, template_img, zfill_length, disable_async),
  53. )
  54. ]
  55. for index, structure in enumerate(slidegen.chosen_structure):
  56. structure_element_splitted: list = slidegen.songtext[
  57. structure
  58. ].splitlines()
  59. line_count = len(structure_element_splitted)
  60. use_line_ranges_per_index = []
  61. use_lines_per_index = []
  62. if line_count <= const.STRUCTURE_ELEMENT_MAX_LINES:
  63. inner_slide_count = 1
  64. else:
  65. inner_slide_count: int = (
  66. line_count // const.STRUCTURE_ELEMENT_MAX_LINES + 1
  67. )
  68. use_lines_per_index = [
  69. line_count // inner_slide_count
  70. ] * inner_slide_count
  71. for inner_slide in range(inner_slide_count):
  72. if sum(use_lines_per_index) == line_count:
  73. break
  74. use_lines_per_index[inner_slide] = (
  75. use_lines_per_index[inner_slide] + 1
  76. )
  77. for inner_slide in range(inner_slide_count):
  78. use_line_ranges_per_index.append(
  79. sum(use_lines_per_index[:inner_slide])
  80. )
  81. for inner_slide in range(inner_slide_count):
  82. current_slide_index += 1
  83. log(
  84. "spawning subprocess for song slide [{} / {}]...".format(
  85. current_slide_index, slide_count
  86. ),
  87. color="yellow",
  88. )
  89. if inner_slide_count == 1:
  90. structure_element_value: str = slidegen.songtext[structure]
  91. else:
  92. splitted_wanted_range: list = structure_element_splitted[
  93. use_line_ranges_per_index[
  94. inner_slide
  95. ] : use_line_ranges_per_index[inner_slide]
  96. + use_lines_per_index[inner_slide]
  97. ]
  98. structure_element_value: str = ""
  99. for element in splitted_wanted_range:
  100. structure_element_value += element + "\n"
  101. structure_element_value = structure_element_value[:-1]
  102. threads.append(
  103. Thread(
  104. target=generate_song_slide,
  105. args=(
  106. slidegen.slide_style.song_slide_form,
  107. template_img,
  108. structure_element_value,
  109. slidegen,
  110. index,
  111. inner_slide_count,
  112. inner_slide,
  113. current_slide_index,
  114. zfill_length,
  115. disable_async,
  116. ),
  117. )
  118. )
  119. for thread in threads:
  120. thread.start()
  121. return threads
  122. def generate_start_slide(slidegen, template_img, zfill_length, disable_async):
  123. first_slide = slidegen.slide_style.start_slide_form()
  124. start_slide_img = first_slide.get_slide(
  125. template_img,
  126. slidegen.metadata["book"],
  127. slidegen.metadata["text"],
  128. slidegen.metadata["melody"],
  129. )
  130. start_slide_img.format = const.IMAGE_FORMAT
  131. try:
  132. start_slide_img.save(
  133. filename=path.join(
  134. slidegen.output_dir,
  135. const.FILE_NAMING
  136. + "1".zfill(zfill_length)
  137. + "."
  138. + const.FILE_EXTENSION,
  139. )
  140. )
  141. if disable_async:
  142. log("start slide generated and saved")
  143. except BlobError:
  144. error_msg("could not write start slide to target directory")
  145. def generate_song_slide(
  146. song_slide,
  147. template_img,
  148. structure_element_value,
  149. slidegen,
  150. index,
  151. inner_slide_count,
  152. inner_slide,
  153. current_slide_index,
  154. zfill_length,
  155. disable_async,
  156. ):
  157. song_slide_img = song_slide.get_slide(
  158. self=slidegen.slide_style.song_slide_form(),
  159. template_img=template_img,
  160. slide_text=structure_element_value,
  161. song_structure=slidegen.chosen_structure,
  162. index=index,
  163. use_arrow=bool(
  164. inner_slide_count != 1 and inner_slide != inner_slide_count - 1
  165. ),
  166. )
  167. song_slide_img.format = const.IMAGE_FORMAT
  168. try:
  169. song_slide_img.save(
  170. filename=path.join(
  171. slidegen.output_dir,
  172. const.FILE_NAMING
  173. + str(current_slide_index + 1).zfill(zfill_length)
  174. + "."
  175. + const.FILE_EXTENSION,
  176. )
  177. )
  178. if disable_async:
  179. log("song slide {} generated and saved".format(current_slide_index))
  180. except BlobError:
  181. error_msg(
  182. "could not write song slide {} to target directory".format(
  183. current_slide_index
  184. )
  185. )