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.
- karaoke_gen/audio_fetcher.py +461 -0
- karaoke_gen/audio_processor.py +407 -30
- karaoke_gen/config.py +62 -113
- karaoke_gen/file_handler.py +32 -59
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +148 -67
- karaoke_gen/karaoke_gen.py +270 -61
- karaoke_gen/lyrics_processor.py +13 -1
- karaoke_gen/metadata.py +78 -73
- karaoke_gen/pipeline/__init__.py +87 -0
- karaoke_gen/pipeline/base.py +215 -0
- karaoke_gen/pipeline/context.py +230 -0
- karaoke_gen/pipeline/executors/__init__.py +21 -0
- karaoke_gen/pipeline/executors/local.py +159 -0
- karaoke_gen/pipeline/executors/remote.py +257 -0
- karaoke_gen/pipeline/stages/__init__.py +27 -0
- karaoke_gen/pipeline/stages/finalize.py +202 -0
- karaoke_gen/pipeline/stages/render.py +165 -0
- karaoke_gen/pipeline/stages/screens.py +139 -0
- karaoke_gen/pipeline/stages/separation.py +191 -0
- karaoke_gen/pipeline/stages/transcription.py +191 -0
- karaoke_gen/style_loader.py +531 -0
- karaoke_gen/utils/bulk_cli.py +6 -0
- karaoke_gen/utils/cli_args.py +424 -0
- karaoke_gen/utils/gen_cli.py +26 -261
- karaoke_gen/utils/remote_cli.py +1815 -0
- karaoke_gen/video_background_processor.py +351 -0
- karaoke_gen-0.71.23.dist-info/METADATA +610 -0
- karaoke_gen-0.71.23.dist-info/RECORD +275 -0
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/WHEEL +1 -1
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/entry_points.txt +1 -0
- lyrics_transcriber/__init__.py +10 -0
- lyrics_transcriber/cli/__init__.py +0 -0
- lyrics_transcriber/cli/cli_main.py +285 -0
- lyrics_transcriber/core/__init__.py +0 -0
- lyrics_transcriber/core/config.py +50 -0
- lyrics_transcriber/core/controller.py +520 -0
- lyrics_transcriber/correction/__init__.py +0 -0
- lyrics_transcriber/correction/agentic/__init__.py +9 -0
- lyrics_transcriber/correction/agentic/adapter.py +71 -0
- lyrics_transcriber/correction/agentic/agent.py +313 -0
- lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
- lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
- lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
- lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
- lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
- lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
- lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
- lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
- lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
- lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
- lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
- lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
- lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
- lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
- lyrics_transcriber/correction/agentic/models/enums.py +38 -0
- lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
- lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
- lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
- lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
- lyrics_transcriber/correction/agentic/models/utils.py +19 -0
- lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
- lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
- lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
- lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
- lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
- lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
- lyrics_transcriber/correction/agentic/providers/base.py +36 -0
- lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
- lyrics_transcriber/correction/agentic/providers/config.py +73 -0
- lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
- lyrics_transcriber/correction/agentic/providers/health.py +28 -0
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
- lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
- lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
- lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
- lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
- lyrics_transcriber/correction/agentic/router.py +35 -0
- lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
- lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
- lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
- lyrics_transcriber/correction/anchor_sequence.py +1043 -0
- lyrics_transcriber/correction/corrector.py +760 -0
- lyrics_transcriber/correction/feedback/__init__.py +2 -0
- lyrics_transcriber/correction/feedback/schemas.py +107 -0
- lyrics_transcriber/correction/feedback/store.py +236 -0
- lyrics_transcriber/correction/handlers/__init__.py +0 -0
- lyrics_transcriber/correction/handlers/base.py +52 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
- lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
- lyrics_transcriber/correction/handlers/llm.py +293 -0
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
- lyrics_transcriber/correction/handlers/repeat.py +88 -0
- lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
- lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
- lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
- lyrics_transcriber/correction/handlers/word_operations.py +187 -0
- lyrics_transcriber/correction/operations.py +352 -0
- lyrics_transcriber/correction/phrase_analyzer.py +435 -0
- lyrics_transcriber/correction/text_utils.py +30 -0
- lyrics_transcriber/frontend/.gitignore +23 -0
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
- lyrics_transcriber/frontend/.yarnrc.yml +3 -0
- lyrics_transcriber/frontend/README.md +50 -0
- lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
- lyrics_transcriber/frontend/__init__.py +25 -0
- lyrics_transcriber/frontend/eslint.config.js +28 -0
- lyrics_transcriber/frontend/index.html +18 -0
- lyrics_transcriber/frontend/package.json +42 -0
- lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/public/favicon.ico +0 -0
- lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/src/App.tsx +212 -0
- lyrics_transcriber/frontend/src/api.ts +239 -0
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
- lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
- lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
- lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
- lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +387 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1373 -0
- lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
- lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
- lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +688 -0
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
- lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
- lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
- lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
- lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
- lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
- lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
- lyrics_transcriber/frontend/src/main.tsx +17 -0
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
- lyrics_transcriber/frontend/src/types.js +2 -0
- lyrics_transcriber/frontend/src/types.ts +199 -0
- lyrics_transcriber/frontend/src/validation.ts +132 -0
- lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
- lyrics_transcriber/frontend/tsconfig.app.json +26 -0
- lyrics_transcriber/frontend/tsconfig.json +25 -0
- lyrics_transcriber/frontend/tsconfig.node.json +23 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
- lyrics_transcriber/frontend/update_version.js +11 -0
- lyrics_transcriber/frontend/vite.config.d.ts +2 -0
- lyrics_transcriber/frontend/vite.config.js +10 -0
- lyrics_transcriber/frontend/vite.config.ts +11 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js +42039 -0
- lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
- lyrics_transcriber/frontend/web_assets/index.html +18 -0
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/yarn.lock +3752 -0
- lyrics_transcriber/lyrics/__init__.py +0 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
- lyrics_transcriber/lyrics/file_provider.py +95 -0
- lyrics_transcriber/lyrics/genius.py +384 -0
- lyrics_transcriber/lyrics/lrclib.py +231 -0
- lyrics_transcriber/lyrics/musixmatch.py +156 -0
- lyrics_transcriber/lyrics/spotify.py +290 -0
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/__init__.py +0 -0
- lyrics_transcriber/output/ass/__init__.py +21 -0
- lyrics_transcriber/output/ass/ass.py +2088 -0
- lyrics_transcriber/output/ass/ass_specs.txt +732 -0
- lyrics_transcriber/output/ass/config.py +180 -0
- lyrics_transcriber/output/ass/constants.py +23 -0
- lyrics_transcriber/output/ass/event.py +94 -0
- lyrics_transcriber/output/ass/formatters.py +132 -0
- lyrics_transcriber/output/ass/lyrics_line.py +265 -0
- lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
- lyrics_transcriber/output/ass/section_detector.py +89 -0
- lyrics_transcriber/output/ass/section_screen.py +106 -0
- lyrics_transcriber/output/ass/style.py +187 -0
- lyrics_transcriber/output/cdg.py +619 -0
- lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
- lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
- lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
- lyrics_transcriber/output/cdgmaker/config.py +151 -0
- lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
- lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
- lyrics_transcriber/output/cdgmaker/pack.py +507 -0
- lyrics_transcriber/output/cdgmaker/render.py +346 -0
- lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
- lyrics_transcriber/output/cdgmaker/utils.py +132 -0
- lyrics_transcriber/output/countdown_processor.py +267 -0
- lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
- lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
- lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/arial.ttf +0 -0
- lyrics_transcriber/output/fonts/georgia.ttf +0 -0
- lyrics_transcriber/output/fonts/verdana.ttf +0 -0
- lyrics_transcriber/output/generator.py +257 -0
- lyrics_transcriber/output/lrc_to_cdg.py +61 -0
- lyrics_transcriber/output/lyrics_file.py +102 -0
- lyrics_transcriber/output/plain_text.py +96 -0
- lyrics_transcriber/output/segment_resizer.py +431 -0
- lyrics_transcriber/output/subtitles.py +397 -0
- lyrics_transcriber/output/video.py +544 -0
- lyrics_transcriber/review/__init__.py +0 -0
- lyrics_transcriber/review/server.py +676 -0
- lyrics_transcriber/storage/__init__.py +0 -0
- lyrics_transcriber/storage/dropbox.py +225 -0
- lyrics_transcriber/transcribers/__init__.py +0 -0
- lyrics_transcriber/transcribers/audioshake.py +290 -0
- lyrics_transcriber/transcribers/base_transcriber.py +157 -0
- lyrics_transcriber/transcribers/whisper.py +330 -0
- lyrics_transcriber/types.py +648 -0
- lyrics_transcriber/utils/__init__.py +0 -0
- lyrics_transcriber/utils/word_utils.py +27 -0
- karaoke_gen-0.57.0.dist-info/METADATA +0 -167
- karaoke_gen-0.57.0.dist-info/RECORD +0 -23
- {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
|
+
)
|