lyrics-transcriber 0.20.0__py3-none-any.whl → 0.30.0__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 (32) hide show
  1. lyrics_transcriber/__init__.py +2 -5
  2. lyrics_transcriber/cli/main.py +194 -0
  3. lyrics_transcriber/core/__init__.py +0 -0
  4. lyrics_transcriber/core/controller.py +283 -0
  5. lyrics_transcriber/{corrector.py → core/corrector.py} +0 -1
  6. lyrics_transcriber/core/fetcher.py +143 -0
  7. lyrics_transcriber/output/__init__.py +0 -0
  8. lyrics_transcriber/output/generator.py +210 -0
  9. lyrics_transcriber/storage/__init__.py +0 -0
  10. lyrics_transcriber/storage/dropbox.py +249 -0
  11. lyrics_transcriber/storage/tokens.py +116 -0
  12. lyrics_transcriber/{audioshake_transcriber.py → transcribers/audioshake.py} +44 -15
  13. lyrics_transcriber/transcribers/base.py +31 -0
  14. lyrics_transcriber/transcribers/whisper.py +186 -0
  15. {lyrics_transcriber-0.20.0.dist-info → lyrics_transcriber-0.30.0.dist-info}/METADATA +5 -16
  16. lyrics_transcriber-0.30.0.dist-info/RECORD +22 -0
  17. lyrics_transcriber-0.30.0.dist-info/entry_points.txt +3 -0
  18. lyrics_transcriber/llm_prompts/README.md +0 -10
  19. lyrics_transcriber/llm_prompts/llm_prompt_lyrics_correction_andrew_handwritten_20231118.txt +0 -55
  20. lyrics_transcriber/llm_prompts/llm_prompt_lyrics_correction_gpt_optimised_20231119.txt +0 -36
  21. lyrics_transcriber/llm_prompts/llm_prompt_lyrics_matching_andrew_handwritten_20231118.txt +0 -19
  22. lyrics_transcriber/llm_prompts/promptfooconfig.yaml +0 -61
  23. lyrics_transcriber/llm_prompts/test_data/ABBA-UnderAttack-Genius.txt +0 -48
  24. lyrics_transcriber/transcriber.py +0 -934
  25. lyrics_transcriber/utils/cli.py +0 -179
  26. lyrics_transcriber-0.20.0.dist-info/RECORD +0 -19
  27. lyrics_transcriber-0.20.0.dist-info/entry_points.txt +0 -3
  28. /lyrics_transcriber/{utils → cli}/__init__.py +0 -0
  29. /lyrics_transcriber/{utils → output}/ass.py +0 -0
  30. /lyrics_transcriber/{utils → output}/subtitles.py +0 -0
  31. {lyrics_transcriber-0.20.0.dist-info → lyrics_transcriber-0.30.0.dist-info}/LICENSE +0 -0
  32. {lyrics_transcriber-0.20.0.dist-info → lyrics_transcriber-0.30.0.dist-info}/WHEEL +0 -0
@@ -1,6 +1,3 @@
1
- import warnings
2
-
3
- warnings.simplefilter("ignore")
4
-
5
- from .transcriber import LyricsTranscriber
1
+ from .core.controller import LyricsTranscriber, TranscriberConfig, LyricsConfig, OutputConfig
6
2
 
3
+ __all__ = ["LyricsTranscriber", "TranscriberConfig", "LyricsConfig", "OutputConfig"]
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python
2
+ import argparse
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Dict
7
+ from importlib.metadata import version
8
+ from dotenv import load_dotenv
9
+
10
+ from lyrics_transcriber import LyricsTranscriber
11
+ from lyrics_transcriber.core.controller import TranscriberConfig, LyricsConfig, OutputConfig
12
+
13
+
14
+ def create_arg_parser() -> argparse.ArgumentParser:
15
+ """Create and configure the argument parser."""
16
+ parser = argparse.ArgumentParser(
17
+ description="Create synchronised lyrics files in ASS and MidiCo LRC formats with word-level timestamps",
18
+ formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=52),
19
+ )
20
+
21
+ # Required arguments
22
+ parser.add_argument(
23
+ "audio_filepath",
24
+ nargs="?",
25
+ help="The audio file path to transcribe lyrics for.",
26
+ default=argparse.SUPPRESS,
27
+ )
28
+
29
+ # Version
30
+ package_version = version("lyrics-transcriber")
31
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {package_version}")
32
+
33
+ # Optional arguments
34
+ parser.add_argument(
35
+ "--log_level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help="Logging level. Default: INFO"
36
+ )
37
+
38
+ # Song identification
39
+ song_group = parser.add_argument_group("Song Identification")
40
+ song_group.add_argument("--artist", help="Song artist for lyrics lookup and auto-correction")
41
+ song_group.add_argument("--title", help="Song title for lyrics lookup and auto-correction")
42
+
43
+ # API Credentials
44
+ api_group = parser.add_argument_group("API Credentials")
45
+ api_group.add_argument(
46
+ "--audioshake_api_token", help="AudioShake API token for lyrics transcription. Can also use AUDIOSHAKE_API_TOKEN env var."
47
+ )
48
+ api_group.add_argument("--genius_api_token", help="Genius API token for lyrics fetching. Can also use GENIUS_API_TOKEN env var.")
49
+ api_group.add_argument(
50
+ "--spotify_cookie", help="Spotify sp_dc cookie value for lyrics fetching. Can also use SPOTIFY_COOKIE_SP_DC env var."
51
+ )
52
+ api_group.add_argument("--runpod_api_key", help="RunPod API key for Whisper transcription. Can also use RUNPOD_API_KEY env var.")
53
+ api_group.add_argument(
54
+ "--whisper_runpod_id", help="RunPod endpoint ID for Whisper transcription. Can also use WHISPER_RUNPOD_ID env var."
55
+ )
56
+
57
+ # Output options
58
+ output_group = parser.add_argument_group("Output Options")
59
+ output_group.add_argument("--output_dir", type=Path, help="Directory where output files will be saved. Default: current directory")
60
+ output_group.add_argument(
61
+ "--cache_dir",
62
+ type=Path,
63
+ default=Path("/tmp/lyrics-transcriber-cache/"),
64
+ help="Directory to cache downloaded/generated files. Default: /tmp/lyrics-transcriber-cache/",
65
+ )
66
+
67
+ # Video options
68
+ video_group = parser.add_argument_group("Video Options")
69
+ video_group.add_argument("--render_video", action="store_true", help="Render a karaoke video with the generated lyrics")
70
+ video_group.add_argument(
71
+ "--video_resolution", choices=["4k", "1080p", "720p", "360p"], default="360p", help="Resolution of the karaoke video. Default: 360p"
72
+ )
73
+ video_group.add_argument("--video_background_image", type=Path, help="Image file to use for karaoke video background")
74
+ video_group.add_argument(
75
+ "--video_background_color",
76
+ default="black",
77
+ help="Color for karaoke video background (hex format or FFmpeg color name). Default: black",
78
+ )
79
+
80
+ return parser
81
+
82
+
83
+ def get_config_from_env() -> Dict[str, str]:
84
+ """Load configuration from environment variables."""
85
+ load_dotenv()
86
+ return {
87
+ "audioshake_api_token": os.getenv("AUDIOSHAKE_API_TOKEN"),
88
+ "genius_api_token": os.getenv("GENIUS_API_TOKEN"),
89
+ "spotify_cookie": os.getenv("SPOTIFY_COOKIE_SP_DC"),
90
+ "runpod_api_key": os.getenv("RUNPOD_API_KEY"),
91
+ "whisper_runpod_id": os.getenv("WHISPER_RUNPOD_ID"),
92
+ }
93
+
94
+
95
+ def setup_logging(log_level: str) -> logging.Logger:
96
+ """Configure logging with consistent format."""
97
+ logger = logging.getLogger("lyrics_transcriber")
98
+ log_level_enum = getattr(logging, log_level.upper())
99
+ logger.setLevel(log_level_enum)
100
+
101
+ if not logger.handlers:
102
+ handler = logging.StreamHandler()
103
+ formatter = logging.Formatter(fmt="%(asctime)s.%(msecs)03d - %(levelname)s - %(module)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
104
+ handler.setFormatter(formatter)
105
+ logger.addHandler(handler)
106
+
107
+ return logger
108
+
109
+
110
+ def create_configs(args: argparse.Namespace, env_config: Dict[str, str]) -> tuple[TranscriberConfig, LyricsConfig, OutputConfig]:
111
+ """Create configuration objects from arguments and environment variables."""
112
+ transcriber_config = TranscriberConfig(
113
+ audioshake_api_token=args.audioshake_api_token or env_config.get("audioshake_api_token"),
114
+ runpod_api_key=args.runpod_api_key or env_config.get("runpod_api_key"),
115
+ whisper_runpod_id=args.whisper_runpod_id or env_config.get("whisper_runpod_id"),
116
+ )
117
+
118
+ lyrics_config = LyricsConfig(
119
+ genius_api_token=args.genius_api_token or env_config.get("genius_api_token"),
120
+ spotify_cookie=args.spotify_cookie or env_config.get("spotify_cookie"),
121
+ )
122
+
123
+ output_config = OutputConfig(
124
+ output_dir=str(args.output_dir) if args.output_dir else None,
125
+ cache_dir=str(args.cache_dir),
126
+ render_video=args.render_video,
127
+ video_resolution=args.video_resolution,
128
+ video_background_image=str(args.video_background_image) if args.video_background_image else None,
129
+ video_background_color=args.video_background_color,
130
+ )
131
+
132
+ return transcriber_config, lyrics_config, output_config
133
+
134
+
135
+ def validate_args(args: argparse.Namespace, parser: argparse.ArgumentParser, logger: logging.Logger) -> None:
136
+ """Validate command line arguments."""
137
+ if not hasattr(args, "audio_filepath"):
138
+ parser.print_help()
139
+ logger.error("No audio filepath provided")
140
+ exit(1)
141
+
142
+ if not os.path.exists(args.audio_filepath):
143
+ logger.error(f"Audio file not found: {args.audio_filepath}")
144
+ exit(1)
145
+
146
+ if args.artist and not args.title or args.title and not args.artist:
147
+ logger.error("Both artist and title must be provided together")
148
+ exit(1)
149
+
150
+
151
+ def main() -> None:
152
+ """Main entry point for the CLI."""
153
+ parser = create_arg_parser()
154
+ args = parser.parse_args()
155
+
156
+ # Set up logging first
157
+ logger = setup_logging(args.log_level)
158
+
159
+ # Validate arguments
160
+ validate_args(args, parser, logger)
161
+
162
+ # Load environment variables
163
+ env_config = get_config_from_env()
164
+
165
+ # Create configuration objects
166
+ transcriber_config, lyrics_config, output_config = create_configs(args, env_config)
167
+
168
+ try:
169
+ # Initialize and run transcriber
170
+ transcriber = LyricsTranscriber(
171
+ audio_filepath=args.audio_filepath,
172
+ artist=args.artist,
173
+ title=args.title,
174
+ transcriber_config=transcriber_config,
175
+ lyrics_config=lyrics_config,
176
+ output_config=output_config,
177
+ logger=logger,
178
+ )
179
+
180
+ results = transcriber.process()
181
+
182
+ # Log results
183
+ logger.info("*** Success! ***")
184
+
185
+ if results.lrc_filepath:
186
+ logger.info(f"Generated LRC file: {results.lrc_filepath}")
187
+ if results.ass_filepath:
188
+ logger.info(f"Generated ASS file: {results.ass_filepath}")
189
+ if results.video_filepath:
190
+ logger.info(f"Generated video file: {results.video_filepath}")
191
+
192
+ except Exception as e:
193
+ logger.error(f"Processing failed: {str(e)}")
194
+ exit(1)
File without changes
@@ -0,0 +1,283 @@
1
+ import os
2
+ import logging
3
+ from dataclasses import dataclass
4
+ from typing import Dict, Optional, List
5
+ from ..transcribers.base import BaseTranscriber
6
+ from ..transcribers.audioshake import AudioShakeTranscriber
7
+ from ..transcribers.whisper import WhisperTranscriber
8
+ from .fetcher import LyricsFetcher
9
+ from ..output.generator import OutputGenerator
10
+ from .corrector import LyricsTranscriptionCorrector
11
+
12
+
13
+ @dataclass
14
+ class TranscriberConfig:
15
+ """Configuration for transcription services."""
16
+
17
+ audioshake_api_token: Optional[str] = None
18
+ runpod_api_key: Optional[str] = None
19
+ whisper_runpod_id: Optional[str] = None
20
+
21
+
22
+ @dataclass
23
+ class LyricsConfig:
24
+ """Configuration for lyrics services."""
25
+
26
+ genius_api_token: Optional[str] = None
27
+ spotify_cookie: Optional[str] = None
28
+
29
+
30
+ @dataclass
31
+ class OutputConfig:
32
+ """Configuration for output generation."""
33
+
34
+ output_dir: Optional[str] = None
35
+ cache_dir: str = "/tmp/lyrics-transcriber-cache/"
36
+ render_video: bool = False
37
+ video_resolution: str = "360p"
38
+ video_background_image: Optional[str] = None
39
+ video_background_color: str = "black"
40
+
41
+
42
+ @dataclass
43
+ class TranscriptionResult:
44
+ """Holds the results of the transcription and correction process."""
45
+
46
+ # Lyrics from internet sources
47
+ lyrics_text: Optional[str] = None
48
+ lyrics_source: Optional[str] = None
49
+ lyrics_genius: Optional[str] = None
50
+ lyrics_spotify: Optional[str] = None
51
+ spotify_lyrics_data: Optional[Dict] = None
52
+
53
+ # Transcription results
54
+ transcription_whisper: Optional[Dict] = None
55
+ transcription_audioshake: Optional[Dict] = None
56
+ transcription_primary: Optional[Dict] = None
57
+ transcription_corrected: Optional[Dict] = None
58
+
59
+ # Output files
60
+ lrc_filepath: Optional[str] = None
61
+ ass_filepath: Optional[str] = None
62
+ video_filepath: Optional[str] = None
63
+
64
+
65
+ class LyricsTranscriber:
66
+ """
67
+ Controller class that orchestrates the lyrics transcription workflow:
68
+ 1. Fetch lyrics from internet sources
69
+ 2. Run multiple transcription methods
70
+ 3. Correct transcribed lyrics using fetched lyrics
71
+ 4. Generate output formats (LRC, ASS, video)
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ audio_filepath: str,
77
+ artist: Optional[str] = None,
78
+ title: Optional[str] = None,
79
+ transcriber_config: Optional[TranscriberConfig] = None,
80
+ lyrics_config: Optional[LyricsConfig] = None,
81
+ output_config: Optional[OutputConfig] = None,
82
+ lyrics_fetcher: Optional[LyricsFetcher] = None,
83
+ corrector: Optional[LyricsTranscriptionCorrector] = None,
84
+ output_generator: Optional[OutputGenerator] = None,
85
+ logger: Optional[logging.Logger] = None,
86
+ log_level: int = logging.DEBUG,
87
+ log_formatter: Optional[logging.Formatter] = None,
88
+ ):
89
+ # Set up logging
90
+ self.logger = logger or logging.getLogger(__name__)
91
+ if not logger:
92
+ self.logger.setLevel(log_level)
93
+ if not self.logger.handlers:
94
+ handler = logging.StreamHandler()
95
+ formatter = log_formatter or logging.Formatter("%(asctime)s - %(levelname)s - %(module)s - %(message)s")
96
+ handler.setFormatter(formatter)
97
+ self.logger.addHandler(handler)
98
+
99
+ self.logger.debug(f"LyricsTranscriber instantiating with input file: {audio_filepath}")
100
+
101
+ # Store configs (with defaults if not provided)
102
+ self.transcriber_config = transcriber_config or TranscriberConfig()
103
+ self.lyrics_config = lyrics_config or LyricsConfig()
104
+ self.output_config = output_config or OutputConfig()
105
+
106
+ # Basic settings
107
+ self.audio_filepath = audio_filepath
108
+ self.artist = artist
109
+ self.title = title
110
+ self.output_prefix = f"{artist} - {title}" if artist and title else os.path.splitext(os.path.basename(audio_filepath))[0]
111
+
112
+ # Create necessary folders
113
+ os.makedirs(self.output_config.cache_dir, exist_ok=True)
114
+ if self.output_config.output_dir:
115
+ os.makedirs(self.output_config.output_dir, exist_ok=True)
116
+
117
+ # Initialize results
118
+ self.results = TranscriptionResult()
119
+
120
+ # Initialize components (with dependency injection)
121
+ self.transcribers = self._initialize_transcribers()
122
+ self.lyrics_fetcher = lyrics_fetcher or self._initialize_lyrics_fetcher()
123
+ self.corrector = corrector or LyricsTranscriptionCorrector(logger=self.logger)
124
+ self.output_generator = output_generator or self._initialize_output_generator()
125
+
126
+ def _initialize_transcribers(self) -> Dict[str, BaseTranscriber]:
127
+ """Initialize available transcription services."""
128
+ transcribers = {}
129
+
130
+ if self.transcriber_config.audioshake_api_token:
131
+ transcribers["audioshake"] = AudioShakeTranscriber(api_token=self.transcriber_config.audioshake_api_token, logger=self.logger)
132
+
133
+ if self.transcriber_config.runpod_api_key and self.transcriber_config.whisper_runpod_id:
134
+ transcribers["whisper"] = WhisperTranscriber(
135
+ logger=self.logger,
136
+ runpod_api_key=self.transcriber_config.runpod_api_key,
137
+ endpoint_id=self.transcriber_config.whisper_runpod_id,
138
+ )
139
+
140
+ return transcribers
141
+
142
+ def _initialize_lyrics_fetcher(self) -> LyricsFetcher:
143
+ """Initialize lyrics fetching service."""
144
+ return LyricsFetcher(
145
+ genius_api_token=self.lyrics_config.genius_api_token, spotify_cookie=self.lyrics_config.spotify_cookie, logger=self.logger
146
+ )
147
+
148
+ def _initialize_output_generator(self) -> OutputGenerator:
149
+ """Initialize output generation service."""
150
+ return OutputGenerator(
151
+ logger=self.logger,
152
+ output_dir=self.output_config.output_dir,
153
+ cache_dir=self.output_config.cache_dir,
154
+ video_resolution=self.output_config.video_resolution,
155
+ video_background_image=self.output_config.video_background_image,
156
+ video_background_color=self.output_config.video_background_color,
157
+ )
158
+
159
+ def process(self) -> TranscriptionResult:
160
+ """
161
+ Main processing method that orchestrates the entire workflow.
162
+
163
+ Returns:
164
+ TranscriptionResult containing all outputs and generated files.
165
+
166
+ Raises:
167
+ Exception: If a critical error occurs during processing.
168
+ """
169
+ try:
170
+ # Step 1: Fetch lyrics if artist and title are provided
171
+ if self.artist and self.title:
172
+ self.fetch_lyrics()
173
+
174
+ # Step 2: Run transcription
175
+ self.transcribe()
176
+
177
+ # Step 3: Process and correct lyrics
178
+ if self.results.transcription_primary:
179
+ self.correct_lyrics()
180
+
181
+ # Step 4: Generate outputs
182
+ if self.results.transcription_corrected:
183
+ self.generate_outputs()
184
+
185
+ self.logger.info("Processing completed successfully")
186
+ return self.results
187
+
188
+ except Exception as e:
189
+ self.logger.error(f"Error during processing: {str(e)}")
190
+ raise
191
+
192
+ def fetch_lyrics(self) -> None:
193
+ """Fetch lyrics from online sources."""
194
+ self.logger.info(f"Fetching lyrics for {self.artist} - {self.title}")
195
+
196
+ try:
197
+ lyrics_result = self.lyrics_fetcher.fetch_lyrics(self.artist, self.title)
198
+
199
+ # Update results
200
+ self.results.lyrics_text = lyrics_result["lyrics"]
201
+ self.results.lyrics_source = lyrics_result["source"]
202
+ self.results.lyrics_genius = lyrics_result["genius_lyrics"]
203
+ self.results.lyrics_spotify = lyrics_result["spotify_lyrics"]
204
+ self.results.spotify_lyrics_data = lyrics_result.get("spotify_lyrics_data")
205
+
206
+ if lyrics_result["lyrics"]:
207
+ self.logger.info(f"Successfully fetched lyrics from {lyrics_result['source']}")
208
+ else:
209
+ self.logger.warning("No lyrics found from any source")
210
+
211
+ except Exception as e:
212
+ self.logger.error(f"Failed to fetch lyrics: {str(e)}")
213
+ # Don't raise - we can continue without lyrics
214
+
215
+ def transcribe(self) -> None:
216
+ """Run transcription using all available transcribers."""
217
+ self.logger.info("Starting transcription process")
218
+
219
+ for name, transcriber in self.transcribers.items():
220
+ try:
221
+ result = transcriber.transcribe(self.audio_filepath)
222
+
223
+ # Store result based on transcriber type
224
+ if name == "whisper":
225
+ self.results.transcription_whisper = result
226
+ elif name == "audioshake":
227
+ self.results.transcription_audioshake = result
228
+
229
+ # Use first successful transcription as primary
230
+ if not self.results.transcription_primary:
231
+ self.results.transcription_primary = result
232
+
233
+ except Exception as e:
234
+ self.logger.error(f"Transcription failed for {name}: {str(e)}")
235
+ continue
236
+
237
+ def correct_lyrics(self) -> None:
238
+ """Run lyrics correction using transcription and internet lyrics."""
239
+ self.logger.info("Starting lyrics correction process")
240
+
241
+ try:
242
+ # Set input data for correction
243
+ self.corrector.set_input_data(
244
+ spotify_lyrics_data_dict=self.results.spotify_lyrics_data,
245
+ spotify_lyrics_text=self.results.lyrics_spotify,
246
+ genius_lyrics_text=self.results.lyrics_genius,
247
+ transcription_data_dict_whisper=self.results.transcription_whisper,
248
+ transcription_data_dict_audioshake=self.results.transcription_audioshake,
249
+ )
250
+
251
+ # Run correction
252
+ corrected_data = self.corrector.run_corrector()
253
+
254
+ # Store corrected results
255
+ self.results.transcription_corrected = corrected_data
256
+ self.logger.info("Lyrics correction completed")
257
+
258
+ except Exception as e:
259
+ self.logger.error(f"Failed to correct lyrics: {str(e)}")
260
+ # Use uncorrected transcription as fallback
261
+ self.results.transcription_corrected = self.results.transcription_primary
262
+ self.logger.warning("Using uncorrected transcription as fallback")
263
+
264
+ def generate_outputs(self) -> None:
265
+ """Generate output files."""
266
+ self.logger.info("Generating output files")
267
+
268
+ try:
269
+ output_files = self.output_generator.generate_outputs(
270
+ transcription_data=self.results.transcription_corrected,
271
+ output_prefix=self.output_prefix,
272
+ audio_filepath=self.audio_filepath,
273
+ render_video=self.output_config.render_video,
274
+ )
275
+
276
+ # Store output paths
277
+ self.results.lrc_filepath = output_files.get("lrc")
278
+ self.results.ass_filepath = output_files.get("ass")
279
+ self.results.video_filepath = output_files.get("video")
280
+
281
+ except Exception as e:
282
+ self.logger.error(f"Failed to generate outputs: {str(e)}")
283
+ raise
@@ -1,6 +1,5 @@
1
1
  import json
2
2
  import logging
3
- from openai import OpenAI
4
3
  from typing import Dict, Optional
5
4
 
6
5
 
@@ -0,0 +1,143 @@
1
+ import os
2
+ import logging
3
+ import lyricsgenius
4
+ import requests
5
+ from typing import Optional, Dict, Any
6
+
7
+
8
+ class LyricsFetcher:
9
+ """Handles fetching lyrics from various online sources."""
10
+
11
+ def __init__(
12
+ self, genius_api_token: Optional[str] = None, spotify_cookie: Optional[str] = None, logger: Optional[logging.Logger] = None
13
+ ):
14
+ self.logger = logger or logging.getLogger(__name__)
15
+ self.genius_api_token = genius_api_token or os.getenv("GENIUS_API_TOKEN")
16
+ self.spotify_cookie = spotify_cookie or os.getenv("SPOTIFY_COOKIE")
17
+
18
+ # Initialize Genius API client if token provided
19
+ self.genius = None
20
+ if self.genius_api_token:
21
+ self.genius = lyricsgenius.Genius(self.genius_api_token)
22
+ self.genius.verbose = False
23
+ self.genius.remove_section_headers = True
24
+
25
+ def fetch_lyrics(self, artist: str, title: str) -> Dict[str, Any]:
26
+ """
27
+ Fetch lyrics from all available sources.
28
+
29
+ Args:
30
+ artist: Name of the artist
31
+ title: Title of the song
32
+
33
+ Returns:
34
+ Dict containing:
35
+ - genius_lyrics: Lyrics from Genius (if available)
36
+ - spotify_lyrics: Lyrics from Spotify (if available)
37
+ - source: The preferred source ("genius" or "spotify")
38
+ - lyrics: The best lyrics found from any source
39
+ """
40
+ self.logger.info(f"Fetching lyrics for {artist} - {title}")
41
+
42
+ result = {"genius_lyrics": None, "spotify_lyrics": None, "source": None, "lyrics": None}
43
+
44
+ # Try Genius first
45
+ if self.genius:
46
+ try:
47
+ result["genius_lyrics"] = self._fetch_from_genius(artist, title)
48
+ if result["genius_lyrics"]:
49
+ result["source"] = "genius"
50
+ result["lyrics"] = result["genius_lyrics"]
51
+ except Exception as e:
52
+ self.logger.error(f"Failed to fetch lyrics from Genius: {str(e)}")
53
+
54
+ # Try Spotify if Genius failed or wasn't available
55
+ if self.spotify_cookie and not result["lyrics"]:
56
+ try:
57
+ result["spotify_lyrics"] = self._fetch_from_spotify(artist, title)
58
+ if result["spotify_lyrics"]:
59
+ result["source"] = "spotify"
60
+ result["lyrics"] = result["spotify_lyrics"]
61
+ except Exception as e:
62
+ self.logger.error(f"Failed to fetch lyrics from Spotify: {str(e)}")
63
+
64
+ return result
65
+
66
+ def _fetch_from_genius(self, artist: str, title: str) -> Optional[str]:
67
+ """Fetch lyrics from Genius."""
68
+ self.logger.info(f"Searching Genius for {artist} - {title}")
69
+
70
+ try:
71
+ song = self.genius.search_song(title, artist)
72
+ if song:
73
+ self.logger.info("Found lyrics on Genius")
74
+ return song.lyrics
75
+ except Exception as e:
76
+ self.logger.error(f"Error fetching from Genius: {str(e)}")
77
+
78
+ return None
79
+
80
+ def _fetch_from_spotify(self, artist: str, title: str) -> Optional[str]:
81
+ """
82
+ Fetch lyrics from Spotify.
83
+
84
+ Uses the Spotify cookie to authenticate and fetch lyrics for a given song.
85
+ The cookie can be obtained by logging into Spotify Web Player and copying
86
+ the 'sp_dc' cookie value.
87
+ """
88
+ self.logger.info(f"Searching Spotify for {artist} - {title}")
89
+
90
+ if not self.spotify_cookie:
91
+ self.logger.warning("No Spotify cookie provided, skipping Spotify lyrics fetch")
92
+ return None
93
+
94
+ try:
95
+ # First, search for the track
96
+ search_url = "https://api.spotify.com/v1/search"
97
+ headers = {
98
+ "Cookie": f"sp_dc={self.spotify_cookie}",
99
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
100
+ "App-Platform": "WebPlayer",
101
+ }
102
+ params = {"q": f"artist:{artist} track:{title}", "type": "track", "limit": 1}
103
+
104
+ self.logger.debug("Making Spotify search request")
105
+ response = requests.get(search_url, headers=headers, params=params)
106
+ response.raise_for_status()
107
+
108
+ search_data = response.json()
109
+ if not search_data.get("tracks", {}).get("items"):
110
+ self.logger.warning("No tracks found on Spotify")
111
+ return None
112
+
113
+ track = search_data["tracks"]["items"][0]
114
+ track_id = track["id"]
115
+
116
+ # Then, fetch lyrics for the track
117
+ lyrics_url = f"https://api.spotify.com/v1/tracks/{track_id}/lyrics"
118
+
119
+ self.logger.debug("Making Spotify lyrics request")
120
+ lyrics_response = requests.get(lyrics_url, headers=headers)
121
+ lyrics_response.raise_for_status()
122
+
123
+ lyrics_data = lyrics_response.json()
124
+ if not lyrics_data.get("lyrics", {}).get("lines"):
125
+ self.logger.warning("No lyrics found for track on Spotify")
126
+ return None
127
+
128
+ # Combine all lines into a single string
129
+ lyrics_lines = [line["words"] for line in lyrics_data["lyrics"]["lines"] if line.get("words")]
130
+ lyrics = "\n".join(lyrics_lines)
131
+
132
+ self.logger.info("Successfully fetched lyrics from Spotify")
133
+ return lyrics
134
+
135
+ except requests.exceptions.RequestException as e:
136
+ self.logger.error(f"Error making request to Spotify: {str(e)}")
137
+ return None
138
+ except KeyError as e:
139
+ self.logger.error(f"Unexpected response format from Spotify: {str(e)}")
140
+ return None
141
+ except Exception as e:
142
+ self.logger.error(f"Unexpected error fetching from Spotify: {str(e)}")
143
+ return None
File without changes