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
@@ -0,0 +1,461 @@
1
+ """
2
+ Audio Fetcher module - abstraction layer for fetching audio files.
3
+
4
+ This module provides a clean interface for searching and downloading audio files
5
+ using flacfetch, replacing the previous direct yt-dlp usage.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ import tempfile
11
+ from abc import ABC, abstractmethod
12
+ from dataclasses import dataclass
13
+ from typing import List, Optional
14
+
15
+
16
+ @dataclass
17
+ class AudioSearchResult:
18
+ """Represents a single search result for audio."""
19
+
20
+ title: str
21
+ artist: str
22
+ url: str
23
+ provider: str
24
+ duration: Optional[int] = None # Duration in seconds
25
+ quality: Optional[str] = None # e.g., "FLAC", "320kbps", etc.
26
+ source_id: Optional[str] = None # Unique ID from the source
27
+ # Raw result object from the provider (for download)
28
+ raw_result: Optional[object] = None
29
+
30
+
31
+ @dataclass
32
+ class AudioFetchResult:
33
+ """Result of an audio fetch operation."""
34
+
35
+ filepath: str
36
+ artist: str
37
+ title: str
38
+ provider: str
39
+ duration: Optional[int] = None
40
+ quality: Optional[str] = None
41
+
42
+
43
+ class AudioFetcherError(Exception):
44
+ """Base exception for audio fetcher errors."""
45
+
46
+ pass
47
+
48
+
49
+ class NoResultsError(AudioFetcherError):
50
+ """Raised when no search results are found."""
51
+
52
+ pass
53
+
54
+
55
+ class DownloadError(AudioFetcherError):
56
+ """Raised when download fails."""
57
+
58
+ pass
59
+
60
+
61
+ class AudioFetcher(ABC):
62
+ """Abstract base class for audio fetching implementations."""
63
+
64
+ @abstractmethod
65
+ def search(self, artist: str, title: str) -> List[AudioSearchResult]:
66
+ """
67
+ Search for audio matching the given artist and title.
68
+
69
+ Args:
70
+ artist: The artist name to search for
71
+ title: The track title to search for
72
+
73
+ Returns:
74
+ List of AudioSearchResult objects
75
+
76
+ Raises:
77
+ NoResultsError: If no results are found
78
+ AudioFetcherError: For other errors
79
+ """
80
+ pass
81
+
82
+ @abstractmethod
83
+ def download(
84
+ self,
85
+ result: AudioSearchResult,
86
+ output_dir: str,
87
+ output_filename: Optional[str] = None,
88
+ ) -> AudioFetchResult:
89
+ """
90
+ Download audio from a search result.
91
+
92
+ Args:
93
+ result: The search result to download
94
+ output_dir: Directory to save the downloaded file
95
+ output_filename: Optional filename (without extension)
96
+
97
+ Returns:
98
+ AudioFetchResult with the downloaded file path
99
+
100
+ Raises:
101
+ DownloadError: If download fails
102
+ """
103
+ pass
104
+
105
+ @abstractmethod
106
+ def search_and_download(
107
+ self,
108
+ artist: str,
109
+ title: str,
110
+ output_dir: str,
111
+ output_filename: Optional[str] = None,
112
+ auto_select: bool = False,
113
+ ) -> AudioFetchResult:
114
+ """
115
+ Search for audio and download it in one operation.
116
+
117
+ In interactive mode (auto_select=False), this will present options to the user.
118
+ In auto mode (auto_select=True), this will automatically select the best result.
119
+
120
+ Args:
121
+ artist: The artist name to search for
122
+ title: The track title to search for
123
+ output_dir: Directory to save the downloaded file
124
+ output_filename: Optional filename (without extension)
125
+ auto_select: If True, automatically select the best result
126
+
127
+ Returns:
128
+ AudioFetchResult with the downloaded file path
129
+
130
+ Raises:
131
+ NoResultsError: If no results are found
132
+ DownloadError: If download fails
133
+ """
134
+ pass
135
+
136
+
137
+ class FlacFetchAudioFetcher(AudioFetcher):
138
+ """
139
+ Audio fetcher implementation using flacfetch library.
140
+
141
+ This provides access to multiple audio sources including private music trackers
142
+ and YouTube, with intelligent prioritization of high-quality sources.
143
+ """
144
+
145
+ def __init__(
146
+ self,
147
+ logger: Optional[logging.Logger] = None,
148
+ redacted_api_key: Optional[str] = None,
149
+ ops_api_key: Optional[str] = None,
150
+ provider_priority: Optional[List[str]] = None,
151
+ ):
152
+ """
153
+ Initialize the FlacFetch audio fetcher.
154
+
155
+ Args:
156
+ logger: Logger instance for output
157
+ redacted_api_key: API key for Redacted tracker (optional)
158
+ ops_api_key: API key for OPS tracker (optional)
159
+ provider_priority: Custom provider priority order (optional)
160
+ """
161
+ self.logger = logger or logging.getLogger(__name__)
162
+ self._redacted_api_key = redacted_api_key or os.environ.get("REDACTED_API_KEY")
163
+ self._ops_api_key = ops_api_key or os.environ.get("OPS_API_KEY")
164
+ self._provider_priority = provider_priority
165
+ self._manager = None
166
+
167
+ def _get_manager(self):
168
+ """Lazily initialize and return the FetchManager."""
169
+ if self._manager is None:
170
+ # Import flacfetch here to avoid import errors if not installed
171
+ from flacfetch.core.manager import FetchManager
172
+ from flacfetch.providers.youtube import YouTubeProvider
173
+
174
+ self._manager = FetchManager()
175
+
176
+ # Add providers based on available API keys
177
+ if self._redacted_api_key:
178
+ from flacfetch.providers.redacted import RedactedProvider
179
+
180
+ self._manager.add_provider(RedactedProvider(api_key=self._redacted_api_key))
181
+ self.logger.debug("Added Redacted provider")
182
+
183
+ if self._ops_api_key:
184
+ from flacfetch.providers.ops import OPSProvider
185
+
186
+ self._manager.add_provider(OPSProvider(api_key=self._ops_api_key))
187
+ self.logger.debug("Added OPS provider")
188
+
189
+ # Always add YouTube as a fallback provider
190
+ self._manager.add_provider(YouTubeProvider())
191
+ self.logger.debug("Added YouTube provider")
192
+
193
+ return self._manager
194
+
195
+ def search(self, artist: str, title: str) -> List[AudioSearchResult]:
196
+ """
197
+ Search for audio matching the given artist and title.
198
+
199
+ Args:
200
+ artist: The artist name to search for
201
+ title: The track title to search for
202
+
203
+ Returns:
204
+ List of AudioSearchResult objects
205
+
206
+ Raises:
207
+ NoResultsError: If no results are found
208
+ """
209
+ from flacfetch.core.models import TrackQuery
210
+
211
+ manager = self._get_manager()
212
+ query = TrackQuery(artist=artist, title=title)
213
+
214
+ self.logger.info(f"Searching for: {artist} - {title}")
215
+ results = manager.search(query)
216
+
217
+ if not results:
218
+ raise NoResultsError(f"No results found for: {artist} - {title}")
219
+
220
+ # Convert to our AudioSearchResult format
221
+ search_results = []
222
+ for result in results:
223
+ search_results.append(
224
+ AudioSearchResult(
225
+ title=getattr(result, "title", title),
226
+ artist=getattr(result, "artist", artist),
227
+ url=getattr(result, "url", ""),
228
+ provider=getattr(result, "provider", "Unknown"),
229
+ duration=getattr(result, "duration", None),
230
+ quality=getattr(result, "quality", None),
231
+ source_id=getattr(result, "id", None),
232
+ raw_result=result,
233
+ )
234
+ )
235
+
236
+ self.logger.info(f"Found {len(search_results)} results")
237
+ return search_results
238
+
239
+ def download(
240
+ self,
241
+ result: AudioSearchResult,
242
+ output_dir: str,
243
+ output_filename: Optional[str] = None,
244
+ ) -> AudioFetchResult:
245
+ """
246
+ Download audio from a search result.
247
+
248
+ Args:
249
+ result: The search result to download
250
+ output_dir: Directory to save the downloaded file
251
+ output_filename: Optional filename (without extension)
252
+
253
+ Returns:
254
+ AudioFetchResult with the downloaded file path
255
+
256
+ Raises:
257
+ DownloadError: If download fails
258
+ """
259
+ manager = self._get_manager()
260
+
261
+ # Ensure output directory exists
262
+ os.makedirs(output_dir, exist_ok=True)
263
+
264
+ # Generate filename if not provided
265
+ if output_filename is None:
266
+ output_filename = f"{result.artist} - {result.title}"
267
+
268
+ self.logger.info(f"Downloading: {result.artist} - {result.title} from {result.provider}")
269
+
270
+ try:
271
+ # Use flacfetch to download
272
+ filepath = manager.download(
273
+ result.raw_result,
274
+ output_path=output_dir,
275
+ output_filename=output_filename,
276
+ )
277
+
278
+ if filepath is None:
279
+ raise DownloadError(f"Download returned no file path for: {result.artist} - {result.title}")
280
+
281
+ self.logger.info(f"Downloaded to: {filepath}")
282
+
283
+ return AudioFetchResult(
284
+ filepath=filepath,
285
+ artist=result.artist,
286
+ title=result.title,
287
+ provider=result.provider,
288
+ duration=result.duration,
289
+ quality=result.quality,
290
+ )
291
+
292
+ except Exception as e:
293
+ raise DownloadError(f"Failed to download {result.artist} - {result.title}: {e}") from e
294
+
295
+ def search_and_download(
296
+ self,
297
+ artist: str,
298
+ title: str,
299
+ output_dir: str,
300
+ output_filename: Optional[str] = None,
301
+ auto_select: bool = False,
302
+ ) -> AudioFetchResult:
303
+ """
304
+ Search for audio and download it in one operation.
305
+
306
+ In interactive mode (auto_select=False), this will present options to the user.
307
+ In auto mode (auto_select=True), this will automatically select the best result.
308
+
309
+ Args:
310
+ artist: The artist name to search for
311
+ title: The track title to search for
312
+ output_dir: Directory to save the downloaded file
313
+ output_filename: Optional filename (without extension)
314
+ auto_select: If True, automatically select the best result
315
+
316
+ Returns:
317
+ AudioFetchResult with the downloaded file path
318
+
319
+ Raises:
320
+ NoResultsError: If no results are found
321
+ DownloadError: If download fails
322
+ """
323
+ from flacfetch.core.models import TrackQuery
324
+
325
+ manager = self._get_manager()
326
+ query = TrackQuery(artist=artist, title=title)
327
+
328
+ self.logger.info(f"Searching for: {artist} - {title}")
329
+ results = manager.search(query)
330
+
331
+ if not results:
332
+ raise NoResultsError(f"No results found for: {artist} - {title}")
333
+
334
+ self.logger.info(f"Found {len(results)} results")
335
+
336
+ if auto_select:
337
+ # Auto mode: select best result based on flacfetch's ranking
338
+ selected = manager.select_best(results)
339
+ self.logger.info(f"Auto-selected: {getattr(selected, 'title', title)} from {getattr(selected, 'provider', 'Unknown')}")
340
+ else:
341
+ # Interactive mode: present options to user
342
+ selected = self._interactive_select(results, artist, title)
343
+
344
+ if selected is None:
345
+ raise NoResultsError(f"No result selected for: {artist} - {title}")
346
+
347
+ # Ensure output directory exists
348
+ os.makedirs(output_dir, exist_ok=True)
349
+
350
+ # Generate filename if not provided
351
+ if output_filename is None:
352
+ output_filename = f"{artist} - {title}"
353
+
354
+ self.logger.info(f"Downloading from {getattr(selected, 'provider', 'Unknown')}...")
355
+
356
+ try:
357
+ filepath = manager.download(
358
+ selected,
359
+ output_path=output_dir,
360
+ output_filename=output_filename,
361
+ )
362
+
363
+ if filepath is None:
364
+ raise DownloadError(f"Download returned no file path for: {artist} - {title}")
365
+
366
+ self.logger.info(f"Downloaded to: {filepath}")
367
+
368
+ return AudioFetchResult(
369
+ filepath=filepath,
370
+ artist=artist,
371
+ title=title,
372
+ provider=getattr(selected, "provider", "Unknown"),
373
+ duration=getattr(selected, "duration", None),
374
+ quality=getattr(selected, "quality", None),
375
+ )
376
+
377
+ except Exception as e:
378
+ raise DownloadError(f"Failed to download {artist} - {title}: {e}") from e
379
+
380
+ def _interactive_select(self, results: list, artist: str, title: str) -> object:
381
+ """
382
+ Present search results to the user for interactive selection.
383
+
384
+ Args:
385
+ results: List of search results from flacfetch
386
+ artist: The artist name being searched
387
+ title: The track title being searched
388
+
389
+ Returns:
390
+ The selected result object
391
+ """
392
+ print(f"\n{'=' * 60}")
393
+ print(f"Search Results for: {artist} - {title}")
394
+ print(f"{'=' * 60}\n")
395
+
396
+ for i, result in enumerate(results, 1):
397
+ provider = getattr(result, "provider", "Unknown")
398
+ result_title = getattr(result, "title", "Unknown")
399
+ result_artist = getattr(result, "artist", "Unknown")
400
+ quality = getattr(result, "quality", "")
401
+ duration = getattr(result, "duration", None)
402
+
403
+ # Format duration if available
404
+ duration_str = ""
405
+ if duration:
406
+ minutes = duration // 60
407
+ seconds = duration % 60
408
+ duration_str = f" [{minutes}:{seconds:02d}]"
409
+
410
+ quality_str = f" ({quality})" if quality else ""
411
+
412
+ print(f" {i}. [{provider}] {result_artist} - {result_title}{quality_str}{duration_str}")
413
+
414
+ print()
415
+ print(" 0. Cancel")
416
+ print()
417
+
418
+ while True:
419
+ try:
420
+ choice = input("Enter your choice (1-{}, or 0 to cancel): ".format(len(results))).strip()
421
+
422
+ if choice == "0":
423
+ self.logger.info("User cancelled selection")
424
+ return None
425
+
426
+ choice_num = int(choice)
427
+ if 1 <= choice_num <= len(results):
428
+ selected = results[choice_num - 1]
429
+ self.logger.info(f"User selected option {choice_num}")
430
+ return selected
431
+ else:
432
+ print(f"Please enter a number between 0 and {len(results)}")
433
+
434
+ except ValueError:
435
+ print("Please enter a valid number")
436
+ except KeyboardInterrupt:
437
+ print("\nCancelled")
438
+ return None
439
+
440
+
441
+ def create_audio_fetcher(
442
+ logger: Optional[logging.Logger] = None,
443
+ redacted_api_key: Optional[str] = None,
444
+ ops_api_key: Optional[str] = None,
445
+ ) -> AudioFetcher:
446
+ """
447
+ Factory function to create an appropriate AudioFetcher instance.
448
+
449
+ Args:
450
+ logger: Logger instance for output
451
+ redacted_api_key: API key for Redacted tracker (optional)
452
+ ops_api_key: API key for OPS tracker (optional)
453
+
454
+ Returns:
455
+ An AudioFetcher instance
456
+ """
457
+ return FlacFetchAudioFetcher(
458
+ logger=logger,
459
+ redacted_api_key=redacted_api_key,
460
+ ops_api_key=ops_api_key,
461
+ )