karaoke-gen 0.57.0__py3-none-any.whl → 0.71.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. karaoke_gen/audio_fetcher.py +461 -0
  2. karaoke_gen/audio_processor.py +407 -30
  3. karaoke_gen/config.py +62 -113
  4. karaoke_gen/file_handler.py +32 -59
  5. karaoke_gen/karaoke_finalise/karaoke_finalise.py +148 -67
  6. karaoke_gen/karaoke_gen.py +270 -61
  7. karaoke_gen/lyrics_processor.py +13 -1
  8. karaoke_gen/metadata.py +78 -73
  9. karaoke_gen/pipeline/__init__.py +87 -0
  10. karaoke_gen/pipeline/base.py +215 -0
  11. karaoke_gen/pipeline/context.py +230 -0
  12. karaoke_gen/pipeline/executors/__init__.py +21 -0
  13. karaoke_gen/pipeline/executors/local.py +159 -0
  14. karaoke_gen/pipeline/executors/remote.py +257 -0
  15. karaoke_gen/pipeline/stages/__init__.py +27 -0
  16. karaoke_gen/pipeline/stages/finalize.py +202 -0
  17. karaoke_gen/pipeline/stages/render.py +165 -0
  18. karaoke_gen/pipeline/stages/screens.py +139 -0
  19. karaoke_gen/pipeline/stages/separation.py +191 -0
  20. karaoke_gen/pipeline/stages/transcription.py +191 -0
  21. karaoke_gen/style_loader.py +531 -0
  22. karaoke_gen/utils/bulk_cli.py +6 -0
  23. karaoke_gen/utils/cli_args.py +424 -0
  24. karaoke_gen/utils/gen_cli.py +26 -261
  25. karaoke_gen/utils/remote_cli.py +1815 -0
  26. karaoke_gen/video_background_processor.py +351 -0
  27. karaoke_gen-0.71.23.dist-info/METADATA +610 -0
  28. karaoke_gen-0.71.23.dist-info/RECORD +275 -0
  29. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/WHEEL +1 -1
  30. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/entry_points.txt +1 -0
  31. lyrics_transcriber/__init__.py +10 -0
  32. lyrics_transcriber/cli/__init__.py +0 -0
  33. lyrics_transcriber/cli/cli_main.py +285 -0
  34. lyrics_transcriber/core/__init__.py +0 -0
  35. lyrics_transcriber/core/config.py +50 -0
  36. lyrics_transcriber/core/controller.py +520 -0
  37. lyrics_transcriber/correction/__init__.py +0 -0
  38. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  39. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  40. lyrics_transcriber/correction/agentic/agent.py +313 -0
  41. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  42. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  43. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  44. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  45. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  46. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  47. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  48. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  49. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  50. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  51. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  52. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  53. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  54. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  55. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  56. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  57. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  58. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  59. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  60. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  61. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  62. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  63. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  64. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  65. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  66. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  67. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  68. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  69. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  70. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  71. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  72. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  73. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  74. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  75. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  76. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  77. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  78. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  79. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  80. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  81. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  82. lyrics_transcriber/correction/agentic/router.py +35 -0
  83. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  84. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  85. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  86. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  87. lyrics_transcriber/correction/anchor_sequence.py +1043 -0
  88. lyrics_transcriber/correction/corrector.py +760 -0
  89. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  90. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  91. lyrics_transcriber/correction/feedback/store.py +236 -0
  92. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  93. lyrics_transcriber/correction/handlers/base.py +52 -0
  94. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  95. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  96. lyrics_transcriber/correction/handlers/llm.py +293 -0
  97. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  98. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  99. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  100. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  101. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  102. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  103. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  104. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  105. lyrics_transcriber/correction/operations.py +352 -0
  106. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  107. lyrics_transcriber/correction/text_utils.py +30 -0
  108. lyrics_transcriber/frontend/.gitignore +23 -0
  109. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  110. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  111. lyrics_transcriber/frontend/README.md +50 -0
  112. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  113. lyrics_transcriber/frontend/__init__.py +25 -0
  114. lyrics_transcriber/frontend/eslint.config.js +28 -0
  115. lyrics_transcriber/frontend/index.html +18 -0
  116. lyrics_transcriber/frontend/package.json +42 -0
  117. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  118. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  119. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  120. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  121. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  122. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  123. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  124. lyrics_transcriber/frontend/src/App.tsx +212 -0
  125. lyrics_transcriber/frontend/src/api.ts +239 -0
  126. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  127. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  128. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  129. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  130. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  131. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  132. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  133. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  134. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  135. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  136. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  137. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  138. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  139. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  140. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  141. lyrics_transcriber/frontend/src/components/Header.tsx +387 -0
  142. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1373 -0
  143. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  144. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  145. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  146. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  147. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  148. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +688 -0
  149. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  150. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  151. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  152. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  153. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  154. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  155. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  157. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  158. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  159. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  160. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  161. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  162. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  163. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  164. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  165. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  166. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  167. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  168. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  169. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  170. lyrics_transcriber/frontend/src/main.tsx +17 -0
  171. lyrics_transcriber/frontend/src/theme.ts +177 -0
  172. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  173. lyrics_transcriber/frontend/src/types.js +2 -0
  174. lyrics_transcriber/frontend/src/types.ts +199 -0
  175. lyrics_transcriber/frontend/src/validation.ts +132 -0
  176. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  177. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  178. lyrics_transcriber/frontend/tsconfig.json +25 -0
  179. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  180. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  181. lyrics_transcriber/frontend/update_version.js +11 -0
  182. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  183. lyrics_transcriber/frontend/vite.config.js +10 -0
  184. lyrics_transcriber/frontend/vite.config.ts +11 -0
  185. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  186. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  187. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  188. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js +42039 -0
  189. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +1 -0
  190. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  191. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  192. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  193. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  194. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  195. lyrics_transcriber/frontend/yarn.lock +3752 -0
  196. lyrics_transcriber/lyrics/__init__.py +0 -0
  197. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  198. lyrics_transcriber/lyrics/file_provider.py +95 -0
  199. lyrics_transcriber/lyrics/genius.py +384 -0
  200. lyrics_transcriber/lyrics/lrclib.py +231 -0
  201. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  202. lyrics_transcriber/lyrics/spotify.py +290 -0
  203. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  204. lyrics_transcriber/output/__init__.py +0 -0
  205. lyrics_transcriber/output/ass/__init__.py +21 -0
  206. lyrics_transcriber/output/ass/ass.py +2088 -0
  207. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  208. lyrics_transcriber/output/ass/config.py +180 -0
  209. lyrics_transcriber/output/ass/constants.py +23 -0
  210. lyrics_transcriber/output/ass/event.py +94 -0
  211. lyrics_transcriber/output/ass/formatters.py +132 -0
  212. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  213. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  214. lyrics_transcriber/output/ass/section_detector.py +89 -0
  215. lyrics_transcriber/output/ass/section_screen.py +106 -0
  216. lyrics_transcriber/output/ass/style.py +187 -0
  217. lyrics_transcriber/output/cdg.py +619 -0
  218. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  219. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  220. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  221. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  222. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  223. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  224. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  225. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  226. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  227. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  228. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  229. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  230. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  231. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  232. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  233. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  234. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  235. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  236. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  237. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  238. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  239. lyrics_transcriber/output/countdown_processor.py +267 -0
  240. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  241. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  242. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  243. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  244. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  245. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  246. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  247. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  248. lyrics_transcriber/output/generator.py +257 -0
  249. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  250. lyrics_transcriber/output/lyrics_file.py +102 -0
  251. lyrics_transcriber/output/plain_text.py +96 -0
  252. lyrics_transcriber/output/segment_resizer.py +431 -0
  253. lyrics_transcriber/output/subtitles.py +397 -0
  254. lyrics_transcriber/output/video.py +544 -0
  255. lyrics_transcriber/review/__init__.py +0 -0
  256. lyrics_transcriber/review/server.py +676 -0
  257. lyrics_transcriber/storage/__init__.py +0 -0
  258. lyrics_transcriber/storage/dropbox.py +225 -0
  259. lyrics_transcriber/transcribers/__init__.py +0 -0
  260. lyrics_transcriber/transcribers/audioshake.py +290 -0
  261. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  262. lyrics_transcriber/transcribers/whisper.py +330 -0
  263. lyrics_transcriber/types.py +648 -0
  264. lyrics_transcriber/utils/__init__.py +0 -0
  265. lyrics_transcriber/utils/word_utils.py +27 -0
  266. karaoke_gen-0.57.0.dist-info/METADATA +0 -167
  267. karaoke_gen-0.57.0.dist-info/RECORD +0 -23
  268. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info/licenses}/LICENSE +0 -0
@@ -28,6 +28,8 @@ from .file_handler import FileHandler
28
28
  from .audio_processor import AudioProcessor
29
29
  from .lyrics_processor import LyricsProcessor
30
30
  from .video_generator import VideoGenerator
31
+ from .video_background_processor import VideoBackgroundProcessor
32
+ from .audio_fetcher import create_audio_fetcher, AudioFetcherError, NoResultsError
31
33
 
32
34
 
33
35
  class KaraokePrep:
@@ -67,10 +69,14 @@ class KaraokePrep:
67
69
  subtitle_offset_ms=0,
68
70
  # Style Configuration
69
71
  style_params_json=None,
72
+ style_overrides=None,
70
73
  # Add the new parameter
71
74
  skip_separation=False,
72
- # YouTube/Online Configuration
73
- cookies_str=None,
75
+ # Video Background Configuration
76
+ background_video=None,
77
+ background_video_darkness=0,
78
+ # Audio Fetcher Configuration
79
+ auto_download=False,
74
80
  ):
75
81
  self.log_level = log_level
76
82
  self.log_formatter = log_formatter
@@ -124,13 +130,30 @@ class KaraokePrep:
124
130
 
125
131
  # Style Config - Keep needed ones
126
132
  self.render_bounding_boxes = render_bounding_boxes # Passed to VideoGenerator
127
- self.style_params_json = style_params_json # Passed to LyricsProcessor
133
+ self.style_params_json = style_params_json
134
+ self.style_overrides = style_overrides
135
+ self.temp_style_file = None
128
136
 
129
- # YouTube/Online Config
130
- self.cookies_str = cookies_str # Passed to metadata extraction and file download
137
+ # Video Background Config
138
+ self.background_video = background_video
139
+ self.background_video_darkness = background_video_darkness
140
+
141
+ # Audio Fetcher Config (replaces yt-dlp)
142
+ self.auto_download = auto_download # If True, automatically select best audio source
143
+
144
+ # Initialize audio fetcher for searching and downloading audio when no input file is provided
145
+ self.audio_fetcher = create_audio_fetcher(logger=self.logger)
131
146
 
132
147
  # Load style parameters using the config module
133
- self.style_params = load_style_params(self.style_params_json, self.logger)
148
+ self.style_params = load_style_params(self.style_params_json, self.style_overrides, self.logger)
149
+
150
+ # If overrides were applied, write to a temp file and update the path
151
+ if self.style_overrides:
152
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".json") as temp_file:
153
+ json.dump(self.style_params, temp_file, indent=2)
154
+ self.temp_style_file = temp_file.name
155
+ self.style_params_json = self.temp_style_file
156
+ self.logger.info(f"Style overrides applied. Using temporary style file: {self.temp_style_file}")
134
157
 
135
158
  # Set up title and end formats using the config module
136
159
  self.title_format = setup_title_format(self.style_params)
@@ -181,6 +204,16 @@ class KaraokePrep:
181
204
  output_jpg=self.output_jpg,
182
205
  )
183
206
 
207
+ # Instantiate VideoBackgroundProcessor if background_video is provided
208
+ if self.background_video:
209
+ self.logger.info(f"Video background enabled: {self.background_video}")
210
+ self.video_background_processor = VideoBackgroundProcessor(
211
+ logger=self.logger,
212
+ ffmpeg_base_command=self.ffmpeg_base_command,
213
+ )
214
+ else:
215
+ self.video_background_processor = None
216
+
184
217
  self.logger.debug(f"Initialized title_format with extra_text: {self.title_format['extra_text']}")
185
218
  self.logger.debug(f"Initialized title_format with extra_text_region: {self.title_format['extra_text_region']}")
186
219
 
@@ -199,10 +232,19 @@ class KaraokePrep:
199
232
  else:
200
233
  self.logger.debug(f"Overall output dir {self.output_dir} already exists")
201
234
 
235
+ def __del__(self):
236
+ # Cleanup the temporary style file if it was created
237
+ if self.temp_style_file and os.path.exists(self.temp_style_file):
238
+ try:
239
+ os.remove(self.temp_style_file)
240
+ self.logger.debug(f"Removed temporary style file: {self.temp_style_file}")
241
+ except OSError as e:
242
+ self.logger.warning(f"Error removing temporary style file {self.temp_style_file}: {e}")
243
+
202
244
  # Compatibility methods for tests - these call the new functions in metadata.py
203
245
  def extract_info_for_online_media(self, input_url=None, input_artist=None, input_title=None):
204
246
  """Compatibility method that calls the function in metadata.py"""
205
- self.extracted_info = extract_info_for_online_media(input_url, input_artist, input_title, self.logger, self.cookies_str)
247
+ self.extracted_info = extract_info_for_online_media(input_url, input_artist, input_title, self.logger)
206
248
  return self.extracted_info
207
249
 
208
250
  def parse_single_track_metadata(self, input_artist, input_title):
@@ -327,51 +369,77 @@ class KaraokePrep:
327
369
  processed_track["input_audio_wav"] = self.file_handler.convert_to_wav(processed_track["input_media"], output_filename_no_extension)
328
370
 
329
371
  else:
330
- # --- URL or Existing Files Handling ---
372
+ # --- AudioFetcher or Existing Files Handling ---
331
373
  # Construct patterns using the determined extractor
332
374
  base_pattern = os.path.join(track_output_dir, f"{artist_title} ({self.extractor}*)")
333
- input_media_glob = glob.glob(f"{base_pattern}.*webm") + glob.glob(f"{base_pattern}.*mp4") # Add other common formats if needed
375
+ input_media_glob = glob.glob(f"{base_pattern}.*flac") + glob.glob(f"{base_pattern}.*mp3") + glob.glob(f"{base_pattern}.*wav") + glob.glob(f"{base_pattern}.*webm") + glob.glob(f"{base_pattern}.*mp4")
334
376
  input_png_glob = glob.glob(f"{base_pattern}.png")
335
377
  input_wav_glob = glob.glob(f"{base_pattern}.wav")
336
378
 
337
- if input_media_glob and input_png_glob and input_wav_glob:
379
+ if input_media_glob and input_wav_glob:
338
380
  # Existing files found
339
381
  processed_track["input_media"] = input_media_glob[0]
340
- processed_track["input_still_image"] = input_png_glob[0]
382
+ processed_track["input_still_image"] = input_png_glob[0] if input_png_glob else None
341
383
  processed_track["input_audio_wav"] = input_wav_glob[0]
342
384
  self.logger.info(f"Found existing media files matching extractor '{self.extractor}', skipping download/conversion.")
343
- # Ensure self.extractor reflects the found files if it was a fallback
344
- # Extract the actual extractor string from the filename if needed, though it should match
345
-
346
- elif self.url: # URL provided and files not found, proceed with download
347
- # Use media_id if available for better uniqueness
348
- filename_suffix = f"{self.extractor} {self.media_id}" if self.media_id else self.extractor
349
- output_filename_no_extension = os.path.join(track_output_dir, f"{artist_title} ({filename_suffix})")
350
-
351
- self.logger.info(f"Downloading input media from {self.url}...")
352
- # Delegate to FileHandler
353
- processed_track["input_media"] = self.file_handler.download_video(self.url, output_filename_no_extension, self.cookies_str)
354
385
 
355
- self.logger.info("Extracting still image from downloaded media (if input is video)...")
356
- # Delegate to FileHandler
357
- processed_track["input_still_image"] = self.file_handler.extract_still_image_from_video(
358
- processed_track["input_media"], output_filename_no_extension
359
- )
360
-
361
- self.logger.info("Converting downloaded video to WAV for audio processing...")
362
- # Delegate to FileHandler
363
- processed_track["input_audio_wav"] = self.file_handler.convert_to_wav(
364
- processed_track["input_media"], output_filename_no_extension
365
- )
386
+ elif getattr(self, '_use_audio_fetcher', False):
387
+ # Use flacfetch to search and download audio
388
+ self.logger.info(f"Using flacfetch to search and download: {self.artist} - {self.title}")
389
+
390
+ try:
391
+ # Search and download audio using the AudioFetcher
392
+ fetch_result = self.audio_fetcher.search_and_download(
393
+ artist=self.artist,
394
+ title=self.title,
395
+ output_dir=track_output_dir,
396
+ output_filename=f"{artist_title} (flacfetch)",
397
+ auto_select=self.auto_download,
398
+ )
399
+
400
+ # Update extractor to reflect the actual provider used
401
+ self.extractor = f"flacfetch-{fetch_result.provider}"
402
+
403
+ # Set up the output paths
404
+ output_filename_no_extension = os.path.join(track_output_dir, f"{artist_title} ({self.extractor})")
405
+
406
+ # Copy/move the downloaded file to the expected location
407
+ processed_track["input_media"] = self.file_handler.download_audio_from_fetcher_result(
408
+ fetch_result.filepath, output_filename_no_extension
409
+ )
410
+
411
+ self.logger.info(f"Audio downloaded from {fetch_result.provider}: {processed_track['input_media']}")
412
+
413
+ # Convert to WAV for audio processing
414
+ self.logger.info("Converting downloaded audio to WAV for processing...")
415
+ processed_track["input_audio_wav"] = self.file_handler.convert_to_wav(
416
+ processed_track["input_media"], output_filename_no_extension
417
+ )
418
+
419
+ # No still image for audio-only downloads
420
+ processed_track["input_still_image"] = None
421
+
422
+ except NoResultsError as e:
423
+ self.logger.error(f"No audio found: {e}")
424
+ return None
425
+ except AudioFetcherError as e:
426
+ self.logger.error(f"Failed to fetch audio: {e}")
427
+ return None
428
+
366
429
  else:
367
- # This case means input_media was None, not a URL, and no existing files found
368
- self.logger.error(f"Cannot proceed: No input file, no URL, and no existing files found for {artist_title}.")
369
- return None
430
+ # This case means input_media was None, no audio fetcher flag, and no existing files found
431
+ self.logger.error(f"Cannot proceed: No input file and no existing files found for {artist_title}.")
432
+ self.logger.error("Please provide a local audio file or use artist+title to search for audio.")
433
+ return None
370
434
 
371
435
  if self.skip_lyrics:
372
436
  self.logger.info("Skipping lyrics fetch as requested.")
373
437
  processed_track["lyrics"] = None
374
438
  processed_track["processed_lyrics"] = None
439
+ # No countdown padding when lyrics are skipped
440
+ processed_track["countdown_padding_added"] = False
441
+ processed_track["countdown_padding_seconds"] = 0.0
442
+ processed_track["padded_vocals_audio"] = None
375
443
  else:
376
444
  lyrics_artist = self.lyrics_artist or self.artist
377
445
  lyrics_title = self.lyrics_title or self.title
@@ -463,6 +531,23 @@ class KaraokePrep:
463
531
  if isinstance(transcriber_outputs, dict):
464
532
  self.lyrics = transcriber_outputs.get("corrected_lyrics_text")
465
533
  processed_track["lyrics"] = transcriber_outputs.get("corrected_lyrics_text_filepath")
534
+
535
+ # Capture countdown padding information
536
+ processed_track["countdown_padding_added"] = transcriber_outputs.get("countdown_padding_added", False)
537
+ processed_track["countdown_padding_seconds"] = transcriber_outputs.get("countdown_padding_seconds", 0.0)
538
+ processed_track["padded_vocals_audio"] = transcriber_outputs.get("padded_audio_filepath")
539
+
540
+ # Store ASS filepath for video background processing
541
+ processed_track["ass_filepath"] = transcriber_outputs.get("ass_filepath")
542
+
543
+ if processed_track["countdown_padding_added"]:
544
+ self.logger.info(
545
+ f"=== COUNTDOWN PADDING DETECTED ==="
546
+ )
547
+ self.logger.info(
548
+ f"Vocals have been padded with {processed_track['countdown_padding_seconds']}s of silence. "
549
+ f"Instrumental tracks will be padded after separation to maintain synchronization."
550
+ )
466
551
  else:
467
552
  self.logger.warning(f"Unexpected type for transcriber_outputs: {type(transcriber_outputs)}, value: {transcriber_outputs}")
468
553
  else:
@@ -476,7 +561,7 @@ class KaraokePrep:
476
561
  # Check if separation_future was the placeholder or a real task
477
562
  # The result index in `results` depends on whether transcription_future existed
478
563
  separation_result_index = 1 if transcription_future else 0
479
- if separation_future is not None and not isinstance(separation_future, asyncio.Task) and len(results) > separation_result_index:
564
+ if separation_future is not None and isinstance(separation_future, asyncio.Task) and len(results) > separation_result_index:
480
565
  self.logger.info("Processing separation results...")
481
566
  try:
482
567
  separation_results = results[separation_result_index]
@@ -506,6 +591,78 @@ class KaraokePrep:
506
591
 
507
592
  self.logger.info("=== Parallel Processing Complete ===")
508
593
 
594
+ # Apply video background if requested and lyrics were processed
595
+ if self.video_background_processor and processed_track.get("lyrics"):
596
+ self.logger.info("=== Processing Video Background ===")
597
+
598
+ # Find the With Vocals video file
599
+ with_vocals_video = os.path.join(track_output_dir, f"{artist_title} (With Vocals).mkv")
600
+
601
+ # Get ASS file from transcriber outputs if available
602
+ ass_file = processed_track.get("ass_filepath")
603
+
604
+ # If not in processed_track, try to find it in common locations
605
+ if not ass_file or not os.path.exists(ass_file):
606
+ self.logger.info("ASS filepath not found in transcriber outputs, searching for it...")
607
+ from .utils import sanitize_filename
608
+ sanitized_artist = sanitize_filename(self.artist)
609
+ sanitized_title = sanitize_filename(self.title)
610
+ lyrics_dir = os.path.join(track_output_dir, "lyrics")
611
+
612
+ possible_ass_files = [
613
+ os.path.join(lyrics_dir, f"{sanitized_artist} - {sanitized_title}.ass"),
614
+ os.path.join(track_output_dir, f"{sanitized_artist} - {sanitized_title}.ass"),
615
+ os.path.join(lyrics_dir, f"{artist_title}.ass"),
616
+ os.path.join(track_output_dir, f"{artist_title}.ass"),
617
+ os.path.join(track_output_dir, f"{artist_title} (Karaoke).ass"),
618
+ os.path.join(lyrics_dir, f"{artist_title} (Karaoke).ass"),
619
+ ]
620
+
621
+ for possible_file in possible_ass_files:
622
+ if os.path.exists(possible_file):
623
+ ass_file = possible_file
624
+ self.logger.info(f"Found ASS subtitle file: {ass_file}")
625
+ break
626
+
627
+ if os.path.exists(with_vocals_video) and ass_file and os.path.exists(ass_file):
628
+ self.logger.info(f"Found With Vocals video, will replace with video background: {with_vocals_video}")
629
+ self.logger.info(f"Using ASS subtitle file: {ass_file}")
630
+
631
+ # Get audio duration
632
+ audio_duration = self.video_background_processor.get_audio_duration(processed_track["input_audio_wav"])
633
+
634
+ # Check if we need to use the padded audio instead
635
+ if processed_track.get("countdown_padding_added") and processed_track.get("padded_vocals_audio"):
636
+ self.logger.info(f"Using padded vocals audio for video background processing")
637
+ audio_for_video = processed_track["padded_vocals_audio"]
638
+ else:
639
+ audio_for_video = processed_track["input_audio_wav"]
640
+
641
+ # Process video background
642
+ try:
643
+ self.video_background_processor.process_video_background(
644
+ video_path=self.background_video,
645
+ audio_path=audio_for_video,
646
+ ass_subtitles_path=ass_file,
647
+ output_path=with_vocals_video,
648
+ darkness_percent=self.background_video_darkness,
649
+ audio_duration=audio_duration,
650
+ )
651
+ self.logger.info(f"✓ Video background applied, With Vocals video updated: {with_vocals_video}")
652
+ except Exception as e:
653
+ self.logger.error(f"Failed to apply video background: {e}")
654
+ self.logger.exception("Full traceback:")
655
+ # Continue with original video if background processing fails
656
+ else:
657
+ if not os.path.exists(with_vocals_video):
658
+ self.logger.warning(f"With Vocals video not found at {with_vocals_video}, skipping video background processing")
659
+ elif not ass_file or not os.path.exists(ass_file):
660
+ self.logger.warning("Could not find ASS subtitle file, skipping video background processing")
661
+ if 'possible_ass_files' in locals():
662
+ self.logger.warning("Searched locations:")
663
+ for possible_file in possible_ass_files:
664
+ self.logger.warning(f" - {possible_file}")
665
+
509
666
  output_image_filepath_noext = os.path.join(track_output_dir, f"{artist_title} (Title)")
510
667
  processed_track["title_image_png"] = f"{output_image_filepath_noext}.png"
511
668
  processed_track["title_image_jpg"] = f"{output_image_filepath_noext}.jpg"
@@ -566,16 +723,58 @@ class KaraokePrep:
566
723
  "instrumental": instrumental_path,
567
724
  "vocals": None,
568
725
  }
569
- else:
570
- # Only run separation if not skipped
571
- if not self.skip_separation:
572
- self.logger.info(f"Separating audio for track: {self.title} by {self.artist}")
573
- # Delegate to AudioProcessor (called directly, not in thread here)
574
- separation_results = self.audio_processor.process_audio_separation(
575
- audio_file=processed_track["input_audio_wav"], artist_title=artist_title, track_output_dir=track_output_dir
726
+
727
+ # If countdown padding was added to vocals, pad the custom instrumental too
728
+ if processed_track.get("countdown_padding_added", False):
729
+ padding_seconds = processed_track["countdown_padding_seconds"]
730
+ self.logger.info(
731
+ f"Countdown padding detected - applying {padding_seconds}s padding to custom instrumental"
576
732
  )
577
- processed_track["separated_audio"] = separation_results
578
- # We don't need an else here, if skip_separation is true, separated_audio remains the default empty dict
733
+
734
+ base, ext = os.path.splitext(instrumental_path)
735
+ padded_instrumental_path = f"{base} (Padded){ext}"
736
+
737
+ if not self.file_handler._file_exists(padded_instrumental_path):
738
+ self.audio_processor.pad_audio_file(instrumental_path, padded_instrumental_path, padding_seconds)
739
+
740
+ # Update the path to use the padded version
741
+ processed_track["separated_audio"]["Custom"]["instrumental"] = padded_instrumental_path
742
+ self.logger.info(f"✓ Custom instrumental has been padded and synchronized with vocals")
743
+ elif "separated_audio" not in processed_track or not processed_track["separated_audio"]:
744
+ # Only run separation if it wasn't already done in parallel processing
745
+ self.logger.info(f"Separation was not completed in parallel processing, running separation for track: {self.title} by {self.artist}")
746
+ # Delegate to AudioProcessor (called directly, not in thread here)
747
+ separation_results = self.audio_processor.process_audio_separation(
748
+ audio_file=processed_track["input_audio_wav"], artist_title=artist_title, track_output_dir=track_output_dir
749
+ )
750
+ processed_track["separated_audio"] = separation_results
751
+ else:
752
+ self.logger.info("Audio separation was already completed in parallel processing, skipping duplicate separation.")
753
+
754
+ # Apply countdown padding to instrumental files if needed
755
+ if processed_track.get("countdown_padding_added", False):
756
+ padding_seconds = processed_track["countdown_padding_seconds"]
757
+ self.logger.info(
758
+ f"=== APPLYING COUNTDOWN PADDING TO INSTRUMENTALS ==="
759
+ )
760
+ self.logger.info(
761
+ f"Applying {padding_seconds}s padding to all instrumental files to sync with vocal countdown"
762
+ )
763
+
764
+ # Apply padding using AudioProcessor
765
+ padded_separation_result = self.audio_processor.apply_countdown_padding_to_instrumentals(
766
+ separation_result=processed_track["separated_audio"],
767
+ padding_seconds=padding_seconds,
768
+ artist_title=artist_title,
769
+ track_output_dir=track_output_dir,
770
+ )
771
+
772
+ # Update processed_track with padded file paths
773
+ processed_track["separated_audio"] = padded_separation_result
774
+
775
+ self.logger.info(
776
+ f"✓ All instrumental files have been padded and are now synchronized with vocals"
777
+ )
579
778
 
580
779
  self.logger.info("Script finished, audio downloaded, lyrics fetched and audio separated!")
581
780
 
@@ -680,21 +879,31 @@ class KaraokePrep:
680
879
  self.logger.info(f"Input media {self.input_media} is a local folder, processing each file individually...")
681
880
  return await self.process_folder()
682
881
  elif self.input_media is not None and os.path.isfile(self.input_media):
683
- self.logger.info(f"Input media {self.input_media} is a local file, youtube logic will be skipped")
882
+ self.logger.info(f"Input media {self.input_media} is a local file, audio download will be skipped")
883
+ return [await self.prep_single_track()]
884
+ elif self.artist and self.title:
885
+ # No input file provided - use flacfetch to search and download audio
886
+ self.logger.info(f"No input file provided, using flacfetch to search for: {self.artist} - {self.title}")
887
+
888
+ # Set up the extracted_info for metadata consistency
889
+ self.extracted_info = {
890
+ "title": f"{self.artist} - {self.title}",
891
+ "artist": self.artist,
892
+ "track_title": self.title,
893
+ "extractor_key": "flacfetch",
894
+ "id": f"flacfetch_{self.artist}_{self.title}".replace(" ", "_"),
895
+ "url": None,
896
+ "source": "flacfetch",
897
+ }
898
+ self.extractor = "flacfetch"
899
+ self.url = None # URL will be determined by flacfetch
900
+
901
+ # Mark that we need to use audio fetcher for download
902
+ self._use_audio_fetcher = True
903
+
684
904
  return [await self.prep_single_track()]
685
905
  else:
686
- self.url = self.input_media
687
- # Use the imported extract_info_for_online_media function
688
- self.extracted_info = extract_info_for_online_media(
689
- input_url=self.url, input_artist=self.artist, input_title=self.title, logger=self.logger, cookies_str=self.cookies_str
906
+ raise ValueError(
907
+ "Either a local file path or both artist and title must be provided. "
908
+ "URL-based input has been replaced with flacfetch audio fetching."
690
909
  )
691
-
692
- if self.extracted_info and "playlist_count" in self.extracted_info:
693
- self.persistent_artist = self.artist
694
- self.logger.info(f"Input URL is a playlist, beginning batch operation with persistent artist: {self.persistent_artist}")
695
- return await self.process_playlist()
696
- else:
697
- self.logger.info(f"Input URL is not a playlist, processing single track")
698
- # Parse metadata to extract artist and title before processing
699
- self.parse_single_track_metadata(self.artist, self.title)
700
- return [await self.prep_single_track()]
@@ -274,7 +274,8 @@ class LyricsProcessor:
274
274
 
275
275
  # Save correction data to JSON file for review interface
276
276
  # Use the expected filename format: "{artist} - {title} (Lyrics Corrections).json"
277
- corrections_filename = f"{filename_artist} - {filename_title} (Lyrics Corrections).json"
277
+ # Use sanitized names to be consistent with all other files created by lyrics_transcriber
278
+ corrections_filename = f"{sanitized_artist} - {sanitized_title} (Lyrics Corrections).json"
278
279
  corrections_filepath = os.path.join(lyrics_dir, corrections_filename)
279
280
 
280
281
  # Use the CorrectionResult's to_dict() method to serialize
@@ -285,6 +286,17 @@ class LyricsProcessor:
285
286
 
286
287
  self.logger.info(f"Saved correction data to {corrections_filepath}")
287
288
 
289
+ # Capture countdown padding information for syncing with instrumental audio
290
+ transcriber_outputs["countdown_padding_added"] = getattr(results, "countdown_padding_added", False)
291
+ transcriber_outputs["countdown_padding_seconds"] = getattr(results, "countdown_padding_seconds", 0.0)
292
+ transcriber_outputs["padded_audio_filepath"] = getattr(results, "padded_audio_filepath", None)
293
+
294
+ if transcriber_outputs["countdown_padding_added"]:
295
+ self.logger.info(
296
+ f"Countdown padding detected: {transcriber_outputs['countdown_padding_seconds']}s added to vocals. "
297
+ f"Instrumental audio will need to be padded accordingly."
298
+ )
299
+
288
300
  if transcriber_outputs:
289
301
  self.logger.info(f"*** Transcriber Filepath Outputs: ***")
290
302
  for key, value in transcriber_outputs.items():
karaoke_gen/metadata.py CHANGED
@@ -1,86 +1,67 @@
1
1
  import logging
2
- import yt_dlp.YoutubeDL as ydl
2
+
3
3
 
4
4
  def extract_info_for_online_media(input_url, input_artist, input_title, logger, cookies_str=None):
5
- """Extracts metadata using yt-dlp, either from a URL or via search."""
6
- logger.info(f"Extracting info for input_url: {input_url} input_artist: {input_artist} input_title: {input_title}")
5
+ """
6
+ Creates metadata info dict from provided artist and title.
7
7
 
8
- # Set up yt-dlp options with enhanced anti-detection
9
- base_opts = {
10
- "quiet": True,
11
- # Anti-detection options
12
- "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
13
- "referer": "https://www.youtube.com/",
14
- "sleep_interval": 1,
15
- "max_sleep_interval": 3,
16
- "fragment_retries": 3,
17
- "extractor_retries": 3,
18
- "retries": 3,
19
- # Headers to appear more human
20
- "http_headers": {
21
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
22
- "Accept-Language": "en-us,en;q=0.5",
23
- "Accept-Encoding": "gzip, deflate",
24
- "DNT": "1",
25
- "Connection": "keep-alive",
26
- "Upgrade-Insecure-Requests": "1",
27
- },
28
- }
8
+ Note: This function no longer supports URL-based metadata extraction.
9
+ Audio search and download is now handled by the AudioFetcher class using flacfetch.
29
10
 
30
- # Add cookies if provided
31
- if cookies_str:
32
- logger.info("Using provided cookies for enhanced YouTube access")
33
- # Save cookies to a temporary file
34
- import tempfile
35
- with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
36
- f.write(cookies_str)
37
- base_opts['cookiefile'] = f.name
38
- else:
39
- logger.info("No cookies provided - attempting standard extraction")
11
+ When both artist and title are provided, this creates a metadata dict that can be
12
+ used by the rest of the pipeline.
40
13
 
41
- extracted_info = None
42
- try:
43
- if input_url is not None:
44
- # If a URL is provided, use it to extract the metadata
45
- with ydl(base_opts) as ydl_instance:
46
- extracted_info = ydl_instance.extract_info(input_url, download=False)
47
- else:
48
- # If no URL is provided, use the query to search for the top result
49
- search_opts = base_opts.copy()
50
- search_opts.update({
51
- "format": "bestaudio",
52
- "noplaylist": "True",
53
- "extract_flat": True
54
- })
55
-
56
- with ydl(search_opts) as ydl_instance:
57
- query = f"{input_artist} {input_title}"
58
- search_results = ydl_instance.extract_info(f"ytsearch1:{query}", download=False)
59
- if search_results and "entries" in search_results and search_results["entries"]:
60
- extracted_info = search_results["entries"][0]
61
- else:
62
- # Raise IndexError to match the expected exception in tests
63
- raise IndexError(f"No search results found on YouTube for query: {input_artist} {input_title}")
64
-
65
- if not extracted_info:
66
- raise Exception(f"Failed to extract info for query: {input_artist} {input_title} or URL: {input_url}")
67
-
68
- return extracted_info
14
+ Args:
15
+ input_url: Deprecated - URLs should be provided as local file paths or use AudioFetcher
16
+ input_artist: The artist name
17
+ input_title: The track title
18
+ logger: Logger instance
19
+ cookies_str: Deprecated - no longer used
20
+
21
+ Returns:
22
+ A dict with metadata if artist and title are provided
69
23
 
70
- finally:
71
- # Clean up temporary cookie file if it was created
72
- if cookies_str and 'cookiefile' in base_opts:
73
- try:
74
- import os
75
- os.unlink(base_opts['cookiefile'])
76
- except:
77
- pass
24
+ Raises:
25
+ ValueError: If URL is provided (deprecated) or if artist/title are missing
26
+ """
27
+ logger.info(f"Extracting info for input_url: {input_url} input_artist: {input_artist} input_title: {input_title}")
28
+
29
+ # URLs are no longer supported - use AudioFetcher for search and download
30
+ if input_url is not None:
31
+ raise ValueError(
32
+ "URL-based audio fetching has been replaced with flacfetch. "
33
+ "Please provide a local file path instead, or use artist and title only "
34
+ "to search for audio via flacfetch."
35
+ )
36
+
37
+ # When artist and title are provided, create a synthetic metadata dict
38
+ # The actual search and download is handled by AudioFetcher
39
+ if input_artist and input_title:
40
+ logger.info(f"Creating metadata for: {input_artist} - {input_title}")
41
+ return {
42
+ "title": f"{input_artist} - {input_title}",
43
+ "artist": input_artist,
44
+ "track_title": input_title,
45
+ "extractor_key": "flacfetch",
46
+ "id": f"flacfetch_{input_artist}_{input_title}".replace(" ", "_"),
47
+ "url": None, # URL will be determined by flacfetch during download
48
+ "source": "flacfetch",
49
+ }
50
+
51
+ # No valid input provided
52
+ raise ValueError(
53
+ f"Artist and title are required for audio search. "
54
+ f"Received artist: {input_artist}, title: {input_title}"
55
+ )
78
56
 
79
57
 
80
58
  def parse_track_metadata(extracted_info, current_artist, current_title, persistent_artist, logger):
81
59
  """
82
60
  Parses extracted_info to determine URL, extractor, ID, artist, and title.
83
61
  Returns a dictionary with the parsed values.
62
+
63
+ This function now supports both legacy yt-dlp style metadata and
64
+ the new flacfetch-based metadata format.
84
65
  """
85
66
  parsed_data = {
86
67
  "url": None,
@@ -93,19 +74,43 @@ def parse_track_metadata(extracted_info, current_artist, current_title, persiste
93
74
  metadata_artist = ""
94
75
  metadata_title = ""
95
76
 
77
+ # Handle flacfetch-style metadata (no URL required)
78
+ if extracted_info.get("source") == "flacfetch":
79
+ parsed_data["url"] = None # URL determined at download time
80
+ parsed_data["extractor"] = "flacfetch"
81
+ parsed_data["media_id"] = extracted_info.get("id")
82
+
83
+ # Use the provided artist/title directly
84
+ if extracted_info.get("artist"):
85
+ parsed_data["artist"] = extracted_info["artist"]
86
+ if extracted_info.get("track_title"):
87
+ parsed_data["title"] = extracted_info["track_title"]
88
+
89
+ if persistent_artist:
90
+ parsed_data["artist"] = persistent_artist
91
+
92
+ logger.info(f"Using flacfetch metadata: artist: {parsed_data['artist']}, title: {parsed_data['title']}")
93
+ return parsed_data
94
+
95
+ # Legacy yt-dlp style metadata handling (for backward compatibility)
96
96
  if "url" in extracted_info:
97
97
  parsed_data["url"] = extracted_info["url"]
98
98
  elif "webpage_url" in extracted_info:
99
99
  parsed_data["url"] = extracted_info["webpage_url"]
100
100
  else:
101
- raise Exception(f"Failed to extract URL from input media metadata: {extracted_info}")
101
+ # For flacfetch results without URL, this is now acceptable
102
+ logger.debug("No URL in extracted info - will be determined at download time")
103
+ parsed_data["url"] = None
102
104
 
103
105
  if "extractor_key" in extracted_info:
104
106
  parsed_data["extractor"] = extracted_info["extractor_key"]
105
107
  elif "ie_key" in extracted_info:
106
108
  parsed_data["extractor"] = extracted_info["ie_key"]
109
+ elif extracted_info.get("source") == "flacfetch":
110
+ parsed_data["extractor"] = "flacfetch"
107
111
  else:
108
- raise Exception(f"Failed to find extractor name from input media metadata: {extracted_info}")
112
+ # Default to flacfetch if no extractor specified
113
+ parsed_data["extractor"] = "flacfetch"
109
114
 
110
115
  if "id" in extracted_info:
111
116
  parsed_data["media_id"] = extracted_info["id"]
@@ -147,7 +152,7 @@ def parse_track_metadata(extracted_info, current_artist, current_title, persiste
147
152
  parsed_data["artist"] = persistent_artist
148
153
 
149
154
  if parsed_data["artist"] and parsed_data["title"]:
150
- logger.info(f"Extracted url: {parsed_data['url']}, artist: {parsed_data['artist']}, title: {parsed_data['title']}")
155
+ logger.info(f"Parsed metadata - artist: {parsed_data['artist']}, title: {parsed_data['title']}")
151
156
  else:
152
157
  logger.debug(extracted_info)
153
158
  raise Exception("Failed to extract artist and title from the input media metadata.")