MonilM commited on
Commit
26adcbb
·
1 Parent(s): b5976f7

HF Space Fix#8

Browse files
Files changed (4) hide show
  1. app.py +299 -92
  2. artist_utils.py +307 -157
  3. cookies.txt +102 -0
  4. settings.py +350 -0
app.py CHANGED
@@ -5,12 +5,31 @@ from typing import List, Dict, Optional
5
  import uvicorn
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from fastapi.staticfiles import StaticFiles
 
8
  import os
9
  import time
 
 
 
 
 
 
 
10
 
11
  # Import recommender and artist utils
12
- from recommendation import MusicRecommender, get_hardcoded_recommendations
13
- from artist_utils import get_bulk_artist_info, load_artist_data, get_artist_info
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  # Configure logging
16
  logging.basicConfig(
@@ -19,11 +38,20 @@ logging.basicConfig(
19
  )
20
  logger = logging.getLogger(__name__)
21
 
 
 
 
 
 
 
 
 
 
22
  # Initialize FastAPI app
23
  app = FastAPI(
24
  title="SongPorter API",
25
- description="Music recommendation and artist info API",
26
- version="1.0.0",
27
  )
28
 
29
  # Add CORS middleware
@@ -37,37 +65,103 @@ app.add_middleware(
37
 
38
  # --- Initialize Recommender ---
39
  recommender = None
40
- try:
41
- logger.info("Initializing Music Recommender...")
42
- recommender = MusicRecommender()
43
- logger.info("Music Recommender loaded successfully.")
44
- except Exception as e:
45
- logger.error(f"Failed to load Music Recommender: {e}", exc_info=True)
 
46
 
47
  # --- Initialize Artist Data ---
48
  if not load_artist_data():
49
  logger.error("CRITICAL: Failed to load artist data on startup.")
50
  # App will run without artist data, but log errors.
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  # --- API Input Models ---
53
 
54
- # --- FIX: Modify ArtistInfoRequestData to accept 'artist_name' ---
55
  class ArtistInfoRequestData(BaseModel):
56
- artist_name: str = Field(..., example="Artist Name 1") # Changed from artist_names: List[str]
57
 
58
- # --- FIX: Define input structure for songs in recommendation request ---
59
  class SongInput(BaseModel):
60
  spotify_id: Optional[str] = None
61
  title: Optional[str] = None
62
  artist: Optional[str] = None
63
 
64
- # --- FIX: Modify RecommendationRequestData ---
65
  class RecommendationRequestData(BaseModel):
66
- # Remove recent_song_ids and top_genres as they are not sent
67
- # recent_song_ids: List[str] = Field(..., example=["spotify_id_1", "song_name_2"])
68
- # top_genres: List[str] = Field(..., example=["Pop", "Rock"])
69
  songs: List[SongInput] = Field(..., example=[{"spotify_id": "id1", "title": "Song A", "artist": "Artist X"}])
70
- limit: Optional[int] = 10 # Make limit optional, default handled in endpoint
 
 
 
 
 
71
 
72
  # --- Request timing middleware ---
73
  @app.middleware("http")
@@ -82,56 +176,36 @@ async def add_process_time_header(request: Request, call_next):
82
 
83
  @app.post("/recommendations/")
84
  async def get_recommendations_endpoint(request_data: RecommendationRequestData):
 
85
  if recommender is None:
86
  logger.error("Recommender not available.")
 
87
  return {"recommendations": get_hardcoded_recommendations(request_data.limit or 10),
88
  "message": "Recommender unavailable, returning popular songs."}
89
 
90
  try:
91
- # --- FIX: Extract data from the new request_data structure ---
92
- limit = request_data.limit or 10 # Use default if not provided
93
-
94
- # Extract recent_song_ids from the 'songs' list
95
  recent_song_ids = [song.spotify_id for song in request_data.songs if song.spotify_id]
96
-
97
- # Handle missing top_genres - Option 1: Use empty list (recommender might have fallbacks)
98
- top_genres = []
99
- # Option 2: Try to derive genres from artist data if available (more complex)
100
- # artist_names_for_genres = [song.artist for song in request_data.songs if song.artist]
101
- # if artist_names_for_genres:
102
- # artist_info_for_genres = get_bulk_artist_info(artist_names_for_genres)
103
- # genre_set = set()
104
- # for info in artist_info_for_genres.values():
105
- # if info.get('artist_genre') and info['artist_genre'] != 'Unknown':
106
- # genre_set.add(info['artist_genre'])
107
- # top_genres = list(genre_set)[:3] # Limit to 3 derived genres
108
 
109
  logger.info(f"Received recommendation request. Extracted IDs: {recent_song_ids}, Derived Genres: {top_genres}, Limit: {limit}")
110
- # --- ---
111
 
112
- # Process recommendation logic (using extracted/derived data)
113
  all_recommendations = []
114
-
115
- # 1. Content-based from recent songs
116
- for song_id in recent_song_ids[:5]: # Limit seed songs
117
  song_recommendations = recommender.find_similar_songs(song_id, n=20)
118
  if song_recommendations:
119
  all_recommendations.extend(song_recommendations)
120
-
121
- # 2. Genre-based (if genres were derived or if recommender handles empty list)
122
- for genre in top_genres[:3]: # Limit seed genres
123
  if genre and genre != 'Unknown':
124
  genre_recommendations = recommender.get_recommendations_by_genre(genre, n=10)
125
  if genre_recommendations:
126
  all_recommendations.extend(genre_recommendations)
127
 
128
- # 3. Fallback
129
  if not all_recommendations:
130
- logger.warning("No recommendations generated from user data, falling back to popular.")
131
  popular_songs = recommender.get_popular_songs(limit)
132
  return {"recommendations": popular_songs}
133
 
134
- # Combine, deduplicate, rank (existing logic)
135
  recommendation_dict = {}
136
  for rec in all_recommendations:
137
  key = rec.get('spotify_id') or rec.get('title')
@@ -144,9 +218,7 @@ async def get_recommendations_endpoint(request_data: RecommendationRequestData):
144
 
145
  final_recommendations = list(recommendation_dict.values())
146
  final_recommendations.sort(key=lambda x: (x.get('count', 0), x.get('popularity', 0)), reverse=True)
147
-
148
- for rec in final_recommendations:
149
- rec.pop('count', None)
150
 
151
  logger.info(f"Generated {len(final_recommendations[:limit])} recommendations.")
152
  return {"recommendations": final_recommendations[:limit]}
@@ -158,60 +230,197 @@ async def get_recommendations_endpoint(request_data: RecommendationRequestData):
158
 
159
  @app.post("/artist-info/")
160
  async def get_artist_info_endpoint(request_data: ArtistInfoRequestData):
161
- """
162
- Endpoint to get information for a single artist in the format expected by Django views.
163
- Returns data with values directly in braces, not as key-value pairs.
164
- """
165
  try:
166
  artist_name = request_data.artist_name
167
  logger.info(f"Received artist info request for artist: {artist_name}")
168
-
169
  if not artist_name:
170
  raise HTTPException(status_code=400, detail="artist_name field cannot be empty")
171
-
172
- # Get artist info from CSV data
173
- artist_info = get_artist_info(artist_name)
174
  logger.info(f"Returning info for artist: {artist_name}")
175
-
176
  # Return in the exact format expected by Django's ArtistSerializer
 
 
177
  return {
178
- 'artist': {artist_info['artist']},
179
- 'artist_img': {artist_info['artist_img']},
180
- 'country': {artist_info['country']},
181
- 'artist_genre': {artist_info['artist_genre']}
182
  }
183
-
184
  except Exception as e:
185
  logger.error(f"Error fetching artist info: {e}", exc_info=True)
186
  raise HTTPException(status_code=500, detail=f"Failed to fetch artist info: {str(e)}")
187
 
188
- @app.post("/artists/")
189
- async def get_multiple_artist_info(artist_names: List[str] = Body(...)):
 
190
  """
191
- Get information for multiple artists in one request.
192
- Format matches the single artist endpoint with values directly in braces, not as key-value pairs.
193
  """
 
 
 
 
 
 
 
 
 
 
194
  try:
195
- logger.info(f"Received request for multiple artists: {artist_names}")
196
-
197
- results = {}
198
-
199
- for name in artist_names:
200
- info = get_artist_info(name)
201
-
202
- # Format response in the expected format with values directly in braces
203
- results[name] = {
204
- 'artist': {info['artist']},
205
- 'artist_img': {info['artist_img']},
206
- 'country': {info['country']},
207
- 'artist_genre': {info['artist_genre']}
208
- }
209
-
210
- return JSONResponse(content=results)
211
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  except Exception as e:
213
- logger.error(f"Error fetching multiple artist info: {e}", exc_info=True)
214
- raise HTTPException(status_code=500, detail=f"Failed to fetch artist info: {str(e)}")
 
 
 
 
 
 
 
215
 
216
  @app.get("/")
217
  async def root():
@@ -220,9 +429,10 @@ async def root():
220
  "endpoints": [
221
  {"path": "/", "method": "GET", "description": "This help message"},
222
  {"path": "/recommendations/", "method": "POST", "description": "Get song recommendations"},
223
- {"path": "/artist-info/", "method": "POST", "description": "Get artist information"}
 
224
  ],
225
- "version": "1.0.0"
226
  }
227
 
228
  # --- Serve static files if they exist ---
@@ -231,10 +441,7 @@ if os.path.exists(static_dir):
231
  app.mount("/static", StaticFiles(directory=static_dir), name="static")
232
  logger.info(f"Mounted static files from {static_dir}")
233
 
234
- # --- IMPORTANT: This section has been modified to be compatible with Hugging Face ---
235
- # In Hugging Face Spaces, the app is launched directly by the hosting platform,
236
- # so we don't need to call uvicorn.run() ourselves.
237
- # Only use this section for local development
238
  if __name__ == "__main__" and os.environ.get('DEPLOYMENT_ENV') != 'huggingface':
239
- port = int(os.environ.get("PORT", 8000))
240
  uvicorn.run("app:app", host="0.0.0.0", port=port, reload=True)
 
5
  import uvicorn
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from fastapi.staticfiles import StaticFiles
8
+ from fastapi.responses import StreamingResponse, FileResponse # Added FileResponse
9
  import os
10
  import time
11
+ import tempfile # Added
12
+ import shutil # Added
13
+ import re # Added
14
+ import yt_dlp # Added
15
+ from mutagen.mp3 import MP3 # Added
16
+ from mutagen.id3 import ID3, TIT2, TPE1, TALB, APIC, TDRC, COMM # Added
17
+ import requests # Added
18
 
19
  # Import recommender and artist utils
20
+ # Assuming these files exist in the same directory or are importable
21
+ try:
22
+ from recommendation import MusicRecommender, get_hardcoded_recommendations
23
+ # Ensure load_artist_data is imported
24
+ from artist_utils import get_bulk_artist_info, load_artist_data, get_artist_info
25
+ except ImportError:
26
+ print("Warning: Recommendation or artist utils not found. Related endpoints might fail.") # Replaced logger
27
+ MusicRecommender = None
28
+ get_hardcoded_recommendations = lambda limit: []
29
+ get_bulk_artist_info = lambda names: {}
30
+ load_artist_data = lambda: False # Define a dummy function if import fails
31
+ get_artist_info = lambda name: {'artist': name, 'artist_img': None, 'country': 'Unknown', 'artist_genre': 'Unknown'}
32
+
33
 
34
  # Configure logging
35
  logging.basicConfig(
 
38
  )
39
  logger = logging.getLogger(__name__)
40
 
41
+ # --- Initialize Artist Data ---
42
+ print("Attempting to load/verify artist data...")
43
+ if load_artist_data():
44
+ print("Artist data loaded/verified successfully.")
45
+ else:
46
+ # Using print instead of logger.error
47
+ print("CRITICAL: Failed to load artist data on startup. Artist info endpoints might fail.")
48
+ # Consider if the app should exit or continue with degraded functionality
49
+
50
  # Initialize FastAPI app
51
  app = FastAPI(
52
  title="SongPorter API",
53
+ description="Music recommendation, artist info, and download API", # Updated description
54
+ version="1.1.0", # Incremented version
55
  )
56
 
57
  # Add CORS middleware
 
65
 
66
  # --- Initialize Recommender ---
67
  recommender = None
68
+ if MusicRecommender:
69
+ try:
70
+ logger.info("Initializing Music Recommender...")
71
+ recommender = MusicRecommender()
72
+ logger.info("Music Recommender loaded successfully.")
73
+ except Exception as e:
74
+ logger.error(f"Failed to load Music Recommender: {e}", exc_info=True)
75
 
76
  # --- Initialize Artist Data ---
77
  if not load_artist_data():
78
  logger.error("CRITICAL: Failed to load artist data on startup.")
79
  # App will run without artist data, but log errors.
80
 
81
+ # --- Utility Functions (Adapted from download_utils.py) ---
82
+
83
+ def sanitize_filename(filename, max_length=200):
84
+ """Sanitizes a string to be used as a valid filename."""
85
+ sanitized = re.sub(r'[\\/*?:\"<>|]', "", filename)
86
+ sanitized = re.sub(r'\s+', ' ', sanitized).strip()
87
+ if len(sanitized) > max_length:
88
+ last_space = sanitized[:max_length].rfind(' ')
89
+ if last_space != -1:
90
+ sanitized = sanitized[:last_space]
91
+ else:
92
+ sanitized = sanitized[:max_length]
93
+ if not sanitized:
94
+ sanitized = "downloaded_file"
95
+ return sanitized
96
+
97
+ def embed_metadata_fastapi(mp3_path, title, artist, album=None, thumbnail_url=None, year=None, youtube_id=None):
98
+ """Embeds ID3 metadata into an MP3 file (FastAPI context)."""
99
+ try:
100
+ print(f"Embedding metadata into: {mp3_path}") # Replaced logger
101
+ audio = MP3(mp3_path, ID3=ID3)
102
+ if audio.tags is None:
103
+ audio.add_tags()
104
+
105
+ audio.tags.add(TIT2(encoding=3, text=title))
106
+ audio.tags.add(TPE1(encoding=3, text=artist))
107
+ if album:
108
+ audio.tags.add(TALB(encoding=3, text=album))
109
+ if year:
110
+ try:
111
+ audio.tags.add(TDRC(encoding=3, text=str(year)))
112
+ except ValueError:
113
+ print(f"Warning: Invalid year format for metadata: {year}") # Replaced logger
114
+ if youtube_id:
115
+ audio.tags.add(COMM(encoding=3, lang='eng', desc='YouTube ID', text=youtube_id))
116
+
117
+ if thumbnail_url:
118
+ try:
119
+ response = requests.get(thumbnail_url, stream=True, timeout=10)
120
+ response.raise_for_status()
121
+ mime_type = response.headers.get('content-type', 'image/jpeg').lower()
122
+ img_format = None
123
+ if 'jpeg' in mime_type or 'jpg' in mime_type: img_format = 'image/jpeg'
124
+ elif 'png' in mime_type: img_format = 'image/png'
125
+
126
+ if img_format:
127
+ image_data = response.content
128
+ audio.tags.add(APIC(encoding=3, mime=img_format, type=3, desc='Cover', data=image_data))
129
+ print(f"Successfully embedded cover art from {thumbnail_url}") # Replaced logger
130
+ else:
131
+ print(f"Warning: Unsupported image format for cover art: {mime_type}") # Replaced logger
132
+
133
+ except requests.exceptions.RequestException as e:
134
+ print(f"Error: Failed to download cover art from {thumbnail_url}: {e}") # Replaced logger
135
+ except Exception as e:
136
+ print(f"Error: Failed to embed cover art: {e}") # Replaced logger
137
+
138
+ audio.save()
139
+ print(f"Metadata embedded successfully for {os.path.basename(mp3_path)}") # Replaced logger
140
+ return True # Indicate success
141
+
142
+ except Exception as e:
143
+ print(f"Error embedding metadata for {os.path.basename(mp3_path)}: {e}") # Replaced logger
144
+ # Consider adding traceback print here: import traceback; traceback.print_exc()
145
+ return False # Indicate failure
146
+
147
  # --- API Input Models ---
148
 
 
149
  class ArtistInfoRequestData(BaseModel):
150
+ artist_name: str = Field(..., example="Artist Name 1")
151
 
 
152
  class SongInput(BaseModel):
153
  spotify_id: Optional[str] = None
154
  title: Optional[str] = None
155
  artist: Optional[str] = None
156
 
 
157
  class RecommendationRequestData(BaseModel):
 
 
 
158
  songs: List[SongInput] = Field(..., example=[{"spotify_id": "id1", "title": "Song A", "artist": "Artist X"}])
159
+ limit: Optional[int] = 10
160
+
161
+ # --- NEW: Download Request Model ---
162
+ class DownloadRequestData(BaseModel):
163
+ url: str = Field(..., example="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
164
+ format: Optional[str] = Field("mp3", example="mp3") # Default to mp3
165
 
166
  # --- Request timing middleware ---
167
  @app.middleware("http")
 
176
 
177
  @app.post("/recommendations/")
178
  async def get_recommendations_endpoint(request_data: RecommendationRequestData):
179
+ # ... (existing recommendation logic remains unchanged) ...
180
  if recommender is None:
181
  logger.error("Recommender not available.")
182
+ # Return hardcoded or popular songs as fallback
183
  return {"recommendations": get_hardcoded_recommendations(request_data.limit or 10),
184
  "message": "Recommender unavailable, returning popular songs."}
185
 
186
  try:
187
+ limit = request_data.limit or 10
 
 
 
188
  recent_song_ids = [song.spotify_id for song in request_data.songs if song.spotify_id]
189
+ top_genres = [] # Derive if needed, or let recommender handle empty
 
 
 
 
 
 
 
 
 
 
 
190
 
191
  logger.info(f"Received recommendation request. Extracted IDs: {recent_song_ids}, Derived Genres: {top_genres}, Limit: {limit}")
 
192
 
 
193
  all_recommendations = []
194
+ for song_id in recent_song_ids[:5]:
 
 
195
  song_recommendations = recommender.find_similar_songs(song_id, n=20)
196
  if song_recommendations:
197
  all_recommendations.extend(song_recommendations)
198
+ for genre in top_genres[:3]:
 
 
199
  if genre and genre != 'Unknown':
200
  genre_recommendations = recommender.get_recommendations_by_genre(genre, n=10)
201
  if genre_recommendations:
202
  all_recommendations.extend(genre_recommendations)
203
 
 
204
  if not all_recommendations:
205
+ logger.warning("No recommendations generated, falling back to popular.")
206
  popular_songs = recommender.get_popular_songs(limit)
207
  return {"recommendations": popular_songs}
208
 
 
209
  recommendation_dict = {}
210
  for rec in all_recommendations:
211
  key = rec.get('spotify_id') or rec.get('title')
 
218
 
219
  final_recommendations = list(recommendation_dict.values())
220
  final_recommendations.sort(key=lambda x: (x.get('count', 0), x.get('popularity', 0)), reverse=True)
221
+ for rec in final_recommendations: rec.pop('count', None)
 
 
222
 
223
  logger.info(f"Generated {len(final_recommendations[:limit])} recommendations.")
224
  return {"recommendations": final_recommendations[:limit]}
 
230
 
231
  @app.post("/artist-info/")
232
  async def get_artist_info_endpoint(request_data: ArtistInfoRequestData):
233
+ # ... (existing artist info logic remains unchanged) ...
 
 
 
234
  try:
235
  artist_name = request_data.artist_name
236
  logger.info(f"Received artist info request for artist: {artist_name}")
 
237
  if not artist_name:
238
  raise HTTPException(status_code=400, detail="artist_name field cannot be empty")
239
+ artist_info = get_artist_info(artist_name) # Assumes this function exists and works
 
 
240
  logger.info(f"Returning info for artist: {artist_name}")
 
241
  # Return in the exact format expected by Django's ArtistSerializer
242
+ # Note: The original request asked for values directly in braces, which isn't standard JSON.
243
+ # Returning standard JSON key-value pairs. Django side might need adjustment if it expects the odd format.
244
  return {
245
+ 'artist': artist_info.get('artist', artist_name),
246
+ 'artist_img': artist_info.get('artist_img'),
247
+ 'country': artist_info.get('country', 'Unknown'),
248
+ 'artist_genre': artist_info.get('artist_genre', 'Unknown')
249
  }
 
250
  except Exception as e:
251
  logger.error(f"Error fetching artist info: {e}", exc_info=True)
252
  raise HTTPException(status_code=500, detail=f"Failed to fetch artist info: {str(e)}")
253
 
254
+ # --- NEW: YouTube Download Endpoint ---
255
+ @app.post("/download-youtube/")
256
+ async def download_youtube_endpoint(request_data: DownloadRequestData):
257
  """
258
+ Downloads audio from a YouTube URL, embeds metadata, and returns the file.
 
259
  """
260
+ url = request_data.url
261
+ output_format = request_data.format if request_data.format in ['mp3', 'aac'] else 'mp3' # Validate format
262
+ temp_dir = None # Initialize temp_dir
263
+
264
+ # Basic URL validation
265
+ if not ("youtube.com" in url or "youtu.be" in url):
266
+ raise HTTPException(status_code=400, detail="Invalid YouTube URL provided.")
267
+ if 'list=' in url:
268
+ raise HTTPException(status_code=400, detail="Playlist URLs are not supported by this endpoint.")
269
+
270
  try:
271
+ print(f"Received download request for YouTube URL: {url} (Format: {output_format})") # Replaced logger
272
+ temp_dir = tempfile.mkdtemp()
273
+ base_temp_filename = os.path.join(temp_dir, f'download_{int(time.time())}')
274
+
275
+ ydl_opts = {
276
+ 'format': 'bestaudio/best',
277
+ 'postprocessors': [{
278
+ 'key': 'FFmpegExtractAudio',
279
+ 'preferredcodec': output_format,
280
+ 'preferredquality': '192', # Standard quality
281
+ }],
282
+ 'outtmpl': f'{base_temp_filename}.%(ext)s',
283
+ 'noplaylist': True,
284
+ 'quiet': True,
285
+ 'no_warnings': True,
286
+ 'cookiefile': 'cookies.txt', # Added cookie file
287
+ 'logtostderr': False,
288
+ 'ignoreerrors': False,
289
+ 'max_filesize': 500 * 1024 * 1024, # Limit download size to 500MB
290
+ }
291
+
292
+ downloaded_info = None
293
+ final_audio_path = None
294
+
295
+ try:
296
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
297
+ print(f"Starting yt-dlp download for: {url}") # Replaced logger
298
+ info_dict = ydl.extract_info(url, download=True)
299
+ downloaded_info = info_dict # Store info for metadata
300
+ print(f"yt-dlp download finished for: {url}") # Replaced logger
301
+
302
+ # Find the downloaded audio file (yt-dlp might rename it slightly)
303
+ expected_prefix = os.path.basename(base_temp_filename)
304
+ for filename in os.listdir(temp_dir):
305
+ if filename.startswith(expected_prefix) and filename.endswith(f'.{output_format}'):
306
+ final_audio_path = os.path.join(temp_dir, filename)
307
+ print(f"Found downloaded audio file: {final_audio_path}") # Replaced logger
308
+ break
309
+
310
+ if not final_audio_path:
311
+ # Fallback search if exact name match failed
312
+ for filename in os.listdir(temp_dir):
313
+ if filename.endswith(f'.{output_format}'):
314
+ final_audio_path = os.path.join(temp_dir, filename)
315
+ print(f"Warning: Found fallback audio file: {final_audio_path}") # Replaced logger
316
+ break
317
+
318
+ if not final_audio_path or not os.path.exists(final_audio_path):
319
+ raise FileNotFoundError(f"Could not locate the downloaded {output_format} file in {temp_dir}")
320
+
321
+ except yt_dlp.utils.DownloadError as e:
322
+ print(f"Error: yt-dlp download error for {url}: {e}") # Replaced logger
323
+ raise HTTPException(status_code=502, detail=f"Failed to download from YouTube: {e}")
324
+ except FileNotFoundError as e:
325
+ print(f"Error: File not found after download attempt for {url}: {e}") # Replaced logger
326
+ raise HTTPException(status_code=500, detail="Download process failed to produce audio file.")
327
+ except Exception as e:
328
+ print(f"Error: Unexpected error during YouTube download for {url}: {e}") # Replaced logger
329
+ # Consider adding traceback print here: import traceback; traceback.print_exc()
330
+ raise HTTPException(status_code=500, detail=f"An unexpected error occurred during download: {e}")
331
+
332
+ # --- Metadata Embedding ---
333
+ metadata_embedded = False
334
+ if downloaded_info and output_format == 'mp3': # Only embed for mp3 currently
335
+ title = downloaded_info.get('title', 'Unknown Title')
336
+ artist = downloaded_info.get('uploader', downloaded_info.get('channel', 'Unknown Artist'))
337
+ album = downloaded_info.get('album')
338
+ year = downloaded_info.get('upload_date', '')[:4] if downloaded_info.get('upload_date') else None
339
+ youtube_id = downloaded_info.get('id')
340
+ thumbnail_url = downloaded_info.get('thumbnail')
341
+
342
+ metadata_embedded = embed_metadata_fastapi(
343
+ mp3_path=final_audio_path,
344
+ title=title,
345
+ artist=artist,
346
+ album=album,
347
+ thumbnail_url=thumbnail_url,
348
+ year=year,
349
+ youtube_id=youtube_id
350
+ )
351
+ elif output_format != 'mp3':
352
+ print(f"Warning: Metadata embedding skipped for non-mp3 format: {output_format}") # Replaced logger
353
+ else:
354
+ print("Warning: Metadata embedding skipped as download info was not available.") # Replaced logger
355
+
356
+
357
+ # --- Prepare Response ---
358
+ # Generate a user-friendly filename
359
+ final_filename_user = "downloaded_track." + output_format
360
+ if downloaded_info:
361
+ title = downloaded_info.get('title', 'Unknown Title')
362
+ artist = downloaded_info.get('uploader', downloaded_info.get('channel', 'Unknown Artist'))
363
+ final_filename_user = f"{sanitize_filename(title)} - {sanitize_filename(artist)}.{output_format}"
364
+
365
+ # Define headers for FileResponse
366
+ headers = {
367
+ 'Content-Disposition': f'attachment; filename="{final_filename_user}"'
368
+ # Add custom metadata headers for the Django backend
369
+ # These should match what the Django view expects
370
+ }
371
+ if downloaded_info:
372
+ headers['X-Song-Title'] = downloaded_info.get('title', 'Unknown Title')
373
+ headers['X-Song-Artist'] = downloaded_info.get('uploader', downloaded_info.get('channel', 'Unknown Artist'))
374
+ if downloaded_info.get('album'): headers['X-Song-Album'] = downloaded_info.get('album')
375
+ if downloaded_info.get('upload_date'): headers['X-Song-Year'] = downloaded_info.get('upload_date')[:4]
376
+ if downloaded_info.get('thumbnail'): headers['X-Thumbnail-URL'] = downloaded_info.get('thumbnail')
377
+ if downloaded_info.get('id'): headers['X-YouTube-ID'] = downloaded_info.get('id')
378
+ if downloaded_info.get('duration'): headers['X-Duration-Seconds'] = str(int(downloaded_info.get('duration')))
379
+
380
+ print(f"Preparing FileResponse for '{final_filename_user}'") # Replaced logger
381
+
382
+ # Return FileResponse - This automatically handles streaming the file content.
383
+ # The temporary directory needs cleanup *after* the response is sent.
384
+ # FastAPI's BackgroundTasks are suitable for this with FileResponse.
385
+ from fastapi import BackgroundTasks
386
+
387
+ async def cleanup():
388
+ if temp_dir and os.path.exists(temp_dir):
389
+ try:
390
+ shutil.rmtree(temp_dir)
391
+ print(f"Cleaned up temporary directory: {temp_dir}") # Replaced logger
392
+ except Exception as e:
393
+ print(f"Error: Failed to clean up temp directory {temp_dir}: {e}") # Replaced logger
394
+
395
+ return FileResponse(
396
+ path=final_audio_path,
397
+ filename=final_filename_user,
398
+ media_type=f'audio/{output_format}',
399
+ headers=headers,
400
+ background=BackgroundTasks([cleanup]) # Add cleanup as a background task
401
+ )
402
+
403
+ except HTTPException as http_exc:
404
+ # Re-raise HTTPExceptions directly
405
+ print(f"HTTP Exception: {http_exc.status_code} - {http_exc.detail}") # Optional: print HTTP exceptions
406
+ # Cleanup might be needed here too if temp_dir was created before the exception
407
+ if temp_dir and os.path.exists(temp_dir):
408
+ try:
409
+ shutil.rmtree(temp_dir, ignore_errors=True)
410
+ print(f"Cleaned up temp dir due to HTTPException: {temp_dir}")
411
+ except Exception as e:
412
+ print(f"Error cleaning temp dir during HTTPException: {e}")
413
+ raise http_exc
414
  except Exception as e:
415
+ print(f"Error: Unexpected error in download endpoint for {url}: {e}") # Replaced logger
416
+ # Consider adding traceback print here: import traceback; traceback.print_exc()
417
+ if temp_dir and os.path.exists(temp_dir):
418
+ try:
419
+ shutil.rmtree(temp_dir, ignore_errors=True) # Cleanup on unexpected errors
420
+ print(f"Cleaned up temp dir due to unexpected error: {temp_dir}")
421
+ except Exception as e_clean:
422
+ print(f"Error cleaning temp dir during unexpected error handling: {e_clean}")
423
+ raise HTTPException(status_code=500, detail=f"An unexpected server error occurred: {str(e)}")
424
 
425
  @app.get("/")
426
  async def root():
 
429
  "endpoints": [
430
  {"path": "/", "method": "GET", "description": "This help message"},
431
  {"path": "/recommendations/", "method": "POST", "description": "Get song recommendations"},
432
+ {"path": "/artist-info/", "method": "POST", "description": "Get artist information"},
433
+ {"path": "/download-youtube/", "method": "POST", "description": "Download audio from YouTube URL"} # Added endpoint info
434
  ],
435
+ "version": "1.1.0" # Updated version
436
  }
437
 
438
  # --- Serve static files if they exist ---
 
441
  app.mount("/static", StaticFiles(directory=static_dir), name="static")
442
  logger.info(f"Mounted static files from {static_dir}")
443
 
444
+ # --- Run for local development (if not on Hugging Face) ---
 
 
 
445
  if __name__ == "__main__" and os.environ.get('DEPLOYMENT_ENV') != 'huggingface':
446
+ port = int(os.environ.get("PORT", 8000)) # Default to 8000 locally
447
  uvicorn.run("app:app", host="0.0.0.0", port=port, reload=True)
artist_utils.py CHANGED
@@ -1,179 +1,329 @@
1
- import logging
2
- import pandas as pd
3
  import os
4
  import re
5
 
6
- # Configure logging
7
- logger = logging.getLogger(__name__)
 
 
 
 
 
 
8
 
9
- # Global artist data
10
- ARTIST_DATA = None
11
- # Artist map for faster lookups {artist_name_lower: {info}}
12
- ARTIST_MAP = {}
 
 
 
 
13
 
14
- def load_artist_data():
15
- """Load artist data from CSV file and build lookup maps"""
16
- global ARTIST_DATA, ARTIST_MAP
17
-
18
  try:
19
- # Get the directory of the current script
20
- current_dir = os.path.dirname(os.path.abspath(__file__))
21
- datasets_path = os.path.join(current_dir, 'datasets')
22
-
23
- # Load artist dataset
24
- artist_path = os.path.join(datasets_path, 'Global Music Artists.csv')
25
- if not os.path.exists(artist_path):
26
- # Try alternative filename
27
- artist_path = os.path.join(datasets_path, 'Global_Music_Artists.csv')
28
- if not os.path.exists(artist_path):
29
- logger.error(f"Artist dataset not found in datasets directory")
30
- return False
 
 
 
 
 
 
 
 
 
31
 
32
- # Load with appropriate encoding
 
 
 
 
33
  try:
34
- ARTIST_DATA = pd.read_csv(artist_path, on_bad_lines='skip', engine='python', encoding='utf-8')
35
- except UnicodeDecodeError:
36
- logger.warning("UTF-8 decoding failed, trying latin1 for artist CSV")
37
- ARTIST_DATA = pd.read_csv(artist_path, on_bad_lines='skip', engine='python', encoding='latin1')
38
-
39
- logger.info(f"Loaded artist dataset with {len(ARTIST_DATA)} entries")
40
- logger.info(f"Artist columns: {ARTIST_DATA.columns.tolist()}")
41
-
42
- # Build artist lookup map
43
- # Handle both column naming conventions (Original and Fixed)
44
- artist_name_col = None
45
- if 'artist_name' in ARTIST_DATA.columns:
46
- artist_name_col = 'artist_name'
47
- elif 'Artist' in ARTIST_DATA.columns:
48
- artist_name_col = 'Artist'
49
- else:
50
- logger.error(f"Critical: Artist name column not found in artist CSV. Found: {ARTIST_DATA.columns.tolist()}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  return False
52
-
53
- # Map columns to expected names (based on CSV format)
54
- column_mapping = {}
55
- # Default to the CVS column names we've found
56
- if 'artist_name' in ARTIST_DATA.columns:
57
- column_mapping = {
58
- 'name': 'artist_name',
59
- 'image': 'artist_img',
60
- 'country': 'country',
61
- 'genre': 'artist_genre'
62
- }
63
- # Use the alternative mapping if needed
64
- elif 'Artist' in ARTIST_DATA.columns:
65
- column_mapping = {
66
- 'name': 'Artist',
67
- 'image': 'Artist Img' if 'Artist Img' in ARTIST_DATA.columns else 'artist_img',
68
- 'country': 'Country' if 'Country' in ARTIST_DATA.columns else 'country',
69
- 'genre': 'Artist Genre' if 'Artist Genre' in ARTIST_DATA.columns else 'artist_genre'
70
- }
71
 
72
- # Create lookup map with lowercase artist names as keys
73
- ARTIST_DATA['artist_lower'] = ARTIST_DATA[artist_name_col].str.lower()
74
-
75
- # Build the map for faster lookups
76
- for _, row in ARTIST_DATA.iterrows():
77
- artist_lower = row['artist_lower']
78
- ARTIST_MAP[artist_lower] = {
79
- 'artist_name': row[column_mapping['name']],
80
- 'artist_img': row[column_mapping['image']] if column_mapping['image'] in row else '',
81
- 'country': row[column_mapping['country']] if column_mapping['country'] in row else 'Unknown',
82
- 'artist_genre': row[column_mapping['genre']] if column_mapping['genre'] in row else 'Unknown'
83
- }
84
-
85
- logger.info(f"Built artist lookup map with {len(ARTIST_MAP)} entries")
86
- return True
87
-
88
- except Exception as e:
89
- logger.error(f"Error loading artist data: {e}", exc_info=True)
90
- return False
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  def get_artist_info(artist_name):
93
  """
94
- Get artist information from the artist map
95
- Returns information in the format expected by Django views
96
  """
97
  default_img = "https://media.istockphoto.com/id/1298261537/vector/blank-man-profile-head-icon-placeholder.jpg?s=612x612&w=0&k=20&c=CeT1RVWZzQDay4t54ookMaFsdi7ZHVFg2Y5v7hxigCA="
98
-
99
- if not artist_name or not ARTIST_MAP:
100
- return {
101
- 'artist': artist_name or "Unknown Artist",
102
- 'artist_img': default_img,
103
- 'country': 'Unknown',
104
- 'artist_genre': 'Unknown'
105
- }
106
-
107
- artist_lower = artist_name.lower()
108
- artist_info = ARTIST_MAP.get(artist_lower)
109
-
110
- if artist_info:
111
- return {
112
- 'artist': artist_name,
113
- 'artist_img': artist_info.get('artist_img') or default_img,
114
- 'country': artist_info.get('country', 'Unknown'),
115
- 'artist_genre': artist_info.get('artist_genre', 'Unknown')
116
- }
117
- else:
118
- # Try fuzzy matching
119
- best_match = None
120
- best_score = 0
121
-
122
- for key in ARTIST_MAP.keys():
123
- # Simple substring matching
124
- if artist_lower in key or key in artist_lower:
125
- score = len(key) / max(len(key), len(artist_lower))
126
- if score > best_score:
127
- best_score = score
128
- best_match = key
129
-
130
- if best_match and best_score > 0.5: # Threshold for accepting a match
131
- artist_info = ARTIST_MAP.get(best_match)
132
  return {
133
- 'artist': artist_name,
134
- 'artist_img': artist_info.get('artist_img') or default_img,
135
- 'country': artist_info.get('country', 'Unknown'),
136
- 'artist_genre': artist_info.get('artist_genre', 'Unknown')
137
  }
138
-
139
- # Default values if no match
140
- return {
141
- 'artist': artist_name,
142
- 'artist_img': default_img,
143
- 'country': 'Unknown',
144
- 'artist_genre': 'Unknown'
145
- }
 
 
 
 
146
 
147
  def get_bulk_artist_info(artist_names):
148
  """
149
- Get information for multiple artists at once
150
- Returns a dictionary mapping artist names to their information
151
  """
152
  results = {}
153
-
154
- for name in artist_names:
155
- results[name] = get_artist_info(name)
156
-
157
- return results
158
-
159
- def normalize_artist_name(name):
160
- """Normalize artist name for better matching"""
161
- if not name:
162
- return ""
163
-
164
- # Convert to lowercase
165
- name = name.lower()
166
-
167
- # Remove common prefixes
168
- prefixes = ["the ", "dj ", "mc "]
169
- for prefix in prefixes:
170
- if name.startswith(prefix):
171
- name = name[len(prefix):]
172
-
173
- # Remove special characters
174
- name = re.sub(r'[^\w\s]', '', name)
175
-
176
- # Remove extra spaces
177
- name = re.sub(r'\s+', ' ', name).strip()
178
-
179
- return name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import csv
3
  import os
4
  import re
5
 
6
+ # Database path
7
+ DB_NAME = 'artists.db'
8
+ # Get the directory of the current script
9
+ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
10
+ DATASETS_PATH = os.path.join(CURRENT_DIR, 'datasets')
11
+ DB_PATH = os.path.join(DATASETS_PATH, DB_NAME)
12
+ CSV_PATH = os.path.join(DATASETS_PATH, 'Global Music Artists.csv')
13
+ ALT_CSV_PATH = os.path.join(DATASETS_PATH, 'Global_Music_Artists.csv')
14
 
15
+ def _normalize_artist_name_for_db(name):
16
+ """Normalize artist name specifically for DB key/lookup."""
17
+ if not name:
18
+ return ""
19
+ name = str(name).lower().strip()
20
+ # Basic normalization, can be expanded
21
+ name = re.sub(r'\s+', ' ', name)
22
+ return name
23
 
24
+ def _get_db_connection():
25
+ """Establishes a connection to the SQLite database."""
 
 
26
  try:
27
+ conn = sqlite3.connect(DB_PATH)
28
+ conn.row_factory = sqlite3.Row # Return rows as dictionary-like objects
29
+ return conn
30
+ except sqlite3.Error as e:
31
+ print(f"Error connecting to database {DB_PATH}: {e}")
32
+ return None
33
+
34
+ def load_artist_data():
35
+ """
36
+ Load artist data from CSV into SQLite database if it doesn't exist.
37
+ Returns True if DB is ready, False otherwise.
38
+ """
39
+ # Check if CSV exists
40
+ actual_csv_path = None
41
+ if os.path.exists(CSV_PATH):
42
+ actual_csv_path = CSV_PATH
43
+ elif os.path.exists(ALT_CSV_PATH):
44
+ actual_csv_path = ALT_CSV_PATH
45
+ else:
46
+ print(f"Error: Artist CSV not found at {CSV_PATH} or {ALT_CSV_PATH}")
47
+ return False
48
 
49
+ # Check if DB needs to be created/populated
50
+ if not os.path.exists(DB_PATH):
51
+ print(f"Database {DB_PATH} not found. Creating and populating from {actual_csv_path}...")
52
+ os.makedirs(DATASETS_PATH, exist_ok=True) # Ensure datasets directory exists
53
+ conn = None
54
  try:
55
+ conn = _get_db_connection()
56
+ if not conn: return False
57
+ cursor = conn.cursor()
58
+
59
+ # Create table
60
+ cursor.execute('''
61
+ CREATE TABLE IF NOT EXISTS artists (
62
+ artist_name_lower TEXT PRIMARY KEY,
63
+ artist_name TEXT,
64
+ artist_genre TEXT,
65
+ artist_img TEXT,
66
+ country TEXT
67
+ )
68
+ ''')
69
+ conn.commit()
70
+ print("Table 'artists' created successfully (if it didn't exist).")
71
+
72
+ # Read CSV and insert data
73
+ count = 0
74
+ inserted_count = 0
75
+ skipped_count = 0
76
+ encodings_to_try = ['utf-8', 'latin1', 'iso-8859-1'] # Add more if needed
77
+
78
+ for encoding in encodings_to_try:
79
+ try:
80
+ print(f"Trying to read CSV with encoding: {encoding}")
81
+ with open(actual_csv_path, 'r', encoding=encoding, errors='ignore') as csvfile: # Use errors='ignore' as fallback
82
+ # Use csv.DictReader to handle header mapping
83
+ reader = csv.DictReader(csvfile)
84
+ # Dynamically find column names (case-insensitive)
85
+ if not reader.fieldnames:
86
+ print(f"Error: CSV file {actual_csv_path} seems empty or has no header.")
87
+ return False
88
+ headers = [h.lower().strip() for h in reader.fieldnames]
89
+ col_map = {
90
+ # Try common variations for column names
91
+ 'name': next((h for h in reader.fieldnames if h.lower().strip() in ['artist_name', 'artist']), None),
92
+ 'genre': next((h for h in reader.fieldnames if h.lower().strip() in ['artist_genre', 'genre']), None),
93
+ 'img': next((h for h in reader.fieldnames if h.lower().strip() in ['artist_img', 'artist img', 'image']), None),
94
+ 'country': next((h for h in reader.fieldnames if h.lower().strip() == 'country'), None)
95
+ }
96
+
97
+ if not col_map['name']:
98
+ print(f"Error: Could not find 'artist_name' or 'artist' column in CSV header: {reader.fieldnames}")
99
+ return False # Cannot proceed without artist name
100
+
101
+ print(f"CSV Headers mapped: name='{col_map['name']}', genre='{col_map['genre']}', img='{col_map['img']}', country='{col_map['country']}'")
102
+
103
+ for row in reader:
104
+ count += 1
105
+ artist_name_raw = row.get(col_map['name'])
106
+ if not artist_name_raw:
107
+ skipped_count += 1
108
+ continue # Skip rows without an artist name
109
+
110
+ artist_name_lower = _normalize_artist_name_for_db(artist_name_raw)
111
+ if not artist_name_lower: # Skip if normalization results in empty string
112
+ skipped_count += 1
113
+ continue
114
+
115
+ # Prepare data for insertion, using None for missing optional columns
116
+ data = (
117
+ artist_name_lower,
118
+ artist_name_raw,
119
+ row.get(col_map['genre']) if col_map['genre'] else None,
120
+ row.get(col_map['img']) if col_map['img'] else None,
121
+ row.get(col_map['country']) if col_map['country'] else None
122
+ )
123
+
124
+ try:
125
+ cursor.execute('''
126
+ INSERT OR IGNORE INTO artists
127
+ (artist_name_lower, artist_name, artist_genre, artist_img, country)
128
+ VALUES (?, ?, ?, ?, ?)
129
+ ''', data)
130
+ if cursor.rowcount > 0:
131
+ inserted_count += 1
132
+ else:
133
+ # This means the key already existed (due to OR IGNORE)
134
+ skipped_count += 1
135
+ except sqlite3.Error as insert_err:
136
+ print(f"Warning: Error inserting row {count} ({artist_name_raw}): {insert_err}. Skipping row.")
137
+ skipped_count += 1
138
+
139
+ # Commit periodically to avoid holding locks for too long
140
+ if count % 5000 == 0: # Increased commit interval
141
+ conn.commit()
142
+ print(f"Processed {count} rows...")
143
+
144
+ conn.commit() # Final commit
145
+ print(f"Successfully populated database from CSV using {encoding}.")
146
+ print(f"Total rows processed: {count}, Inserted: {inserted_count}, Skipped/Duplicates: {skipped_count}")
147
+ return True # Success
148
+
149
+ except UnicodeDecodeError as ude:
150
+ print(f"Encoding {encoding} failed: {ude}. Trying next encoding.")
151
+ if conn: conn.rollback() # Rollback any partial inserts from the failed encoding attempt
152
+ except FileNotFoundError:
153
+ print(f"Error: CSV file not found at {actual_csv_path}")
154
+ return False
155
+ except Exception as e:
156
+ print(f"Error processing CSV with encoding {encoding}: {e}")
157
+ if conn: conn.rollback()
158
+ # Don't return False immediately, try next encoding
159
+
160
+ # If all encodings failed
161
+ print("Error: Could not read CSV with any attempted encoding.")
162
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
+ except sqlite3.Error as e:
165
+ print(f"Database error during population: {e}")
166
+ if conn: conn.rollback()
167
+ return False
168
+ finally:
169
+ if conn:
170
+ conn.close()
171
+ print("Database connection closed.")
172
+ else:
173
+ print(f"Database {DB_PATH} already exists. Skipping population.")
174
+ # Optional: Add check here to see if CSV is newer than DB and repopulate if needed.
175
+ # Basic check: DB exists, ensure table exists
176
+ conn = None
177
+ try:
178
+ conn = _get_db_connection()
179
+ if not conn: return False # Cannot verify if connection fails
180
+ cursor = conn.cursor()
181
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='artists';")
182
+ if cursor.fetchone():
183
+ print("Table 'artists' confirmed to exist.")
184
+ return True
185
+ else:
186
+ print("Error: Database file exists, but 'artists' table is missing. Consider deleting the .db file and restarting.")
187
+ return False
188
+ except sqlite3.Error as e:
189
+ print(f"Database error checking existing DB: {e}")
190
+ return False
191
+ finally:
192
+ if conn:
193
+ conn.close()
194
+
195
 
196
  def get_artist_info(artist_name):
197
  """
198
+ Get artist information from the SQLite database.
199
+ Returns information in the format expected by Django views.
200
  """
201
  default_img = "https://media.istockphoto.com/id/1298261537/vector/blank-man-profile-head-icon-placeholder.jpg?s=612x612&w=0&k=20&c=CeT1RVWZzQDay4t54ookMaFsdi7ZHVFg2Y5v7hxigCA="
202
+ default_info = {
203
+ 'artist': artist_name or "Unknown Artist",
204
+ 'artist_img': default_img,
205
+ 'country': 'Unknown',
206
+ 'artist_genre': 'Unknown'
207
+ }
208
+
209
+ if not artist_name:
210
+ return default_info
211
+
212
+ artist_lower = _normalize_artist_name_for_db(artist_name)
213
+ if not artist_lower:
214
+ return default_info
215
+
216
+ conn = None
217
+ try:
218
+ conn = _get_db_connection()
219
+ if not conn: return default_info
220
+ cursor = conn.cursor()
221
+
222
+ cursor.execute("SELECT artist_name, artist_genre, artist_img, country FROM artists WHERE artist_name_lower = ?", (artist_lower,))
223
+ row = cursor.fetchone()
224
+
225
+ if row:
226
+ # Use fetched name for consistency, fallback to input name if somehow null
227
+ fetched_artist_name = row['artist_name'] or artist_name
 
 
 
 
 
 
 
 
228
  return {
229
+ 'artist': fetched_artist_name,
230
+ 'artist_img': row['artist_img'] or default_img,
231
+ 'country': row['country'] or 'Unknown',
232
+ 'artist_genre': row['artist_genre'] or 'Unknown'
233
  }
234
+ else:
235
+ # Simple fallback if exact match fails (no fuzzy matching here for performance)
236
+ # print(f"Artist '{artist_name}' (normalized: '{artist_lower}') not found in DB.") # Keep this commented unless debugging
237
+ return default_info
238
+
239
+ except sqlite3.Error as e:
240
+ print(f"Database error fetching artist '{artist_name}': {e}")
241
+ return default_info
242
+ finally:
243
+ if conn:
244
+ conn.close()
245
+
246
 
247
  def get_bulk_artist_info(artist_names):
248
  """
249
+ Get information for multiple artists at once from the SQLite database.
250
+ Returns a dictionary mapping original artist names to their information.
251
  """
252
  results = {}
253
+ default_img = "https://media.istockphoto.com/id/1298261537/vector/blank-man-profile-head-icon-placeholder.jpg?s=612x612&w=0&k=20&c=CeT1RVWZzQDay4t54ookMaFsdi7ZHVFg2Y5v7hxigCA="
254
+
255
+ # Initialize results with default info for all requested names
256
+ valid_artist_names = [name for name in artist_names if name] # Filter out None or empty strings
257
+ for name in valid_artist_names:
258
+ results[name] = {
259
+ 'artist': name,
260
+ 'artist_img': default_img,
261
+ 'country': 'Unknown',
262
+ 'artist_genre': 'Unknown'
263
+ }
264
+ # Add entry for None/empty string if it was in the original list
265
+ if None in artist_names or "" in artist_names:
266
+ if None not in results: results[None] = {'artist': 'Unknown Artist', 'artist_img': default_img, 'country': 'Unknown', 'artist_genre': 'Unknown'}
267
+ if "" not in results: results[""] = {'artist': 'Unknown Artist', 'artist_img': default_img, 'country': 'Unknown', 'artist_genre': 'Unknown'}
268
+
269
+
270
+ # Filter out empty names and normalize valid ones for query
271
+ normalized_map = {} # Map normalized name back to original(s)
272
+ normalized_names_to_query = []
273
+ for name in valid_artist_names:
274
+ normalized = _normalize_artist_name_for_db(name)
275
+ if normalized:
276
+ # Only add unique normalized names to the query list
277
+ if normalized not in normalized_map:
278
+ normalized_names_to_query.append(normalized)
279
+ normalized_map[normalized] = []
280
+ normalized_map[normalized].append(name) # Store original name(s)
281
+
282
+ if not normalized_names_to_query:
283
+ return results # Return defaults if no valid names provided
284
+
285
+ conn = None
286
+ try:
287
+ conn = _get_db_connection()
288
+ if not conn: return results # Return defaults if DB connection fails
289
+ cursor = conn.cursor()
290
+
291
+ # Create placeholders for the IN clause
292
+ placeholders = ','.join('?' * len(normalized_names_to_query))
293
+ query = f"SELECT artist_name_lower, artist_name, artist_genre, artist_img, country FROM artists WHERE artist_name_lower IN ({placeholders})"
294
+
295
+ cursor.execute(query, normalized_names_to_query)
296
+ rows = cursor.fetchall()
297
+
298
+ # Update results with fetched data
299
+ processed_originals = set()
300
+ for row in rows:
301
+ normalized_key = row['artist_name_lower']
302
+ if normalized_key in normalized_map:
303
+ # Update all original names that mapped to this normalized key
304
+ for original_name in normalized_map[normalized_key]:
305
+ # Use fetched name for consistency, fallback to original if somehow null
306
+ fetched_artist_name = row['artist_name'] or original_name
307
+ results[original_name] = {
308
+ 'artist': fetched_artist_name,
309
+ 'artist_img': row['artist_img'] or default_img,
310
+ 'country': row['country'] or 'Unknown',
311
+ 'artist_genre': row['artist_genre'] or 'Unknown'
312
+ }
313
+ processed_originals.add(original_name)
314
+
315
+ # Any original names whose normalized form wasn't found retain defaults
316
+
317
+ return results
318
+
319
+ except sqlite3.Error as e:
320
+ print(f"Database error during bulk fetch: {e}")
321
+ # Return the results dictionary which contains defaults for failed lookups
322
+ return results
323
+ finally:
324
+ if conn:
325
+ conn.close()
326
+
327
+ # Note: The global ARTIST_DATA and ARTIST_MAP are no longer used or needed.
328
+ # The load_artist_data function should be called once at application startup.
329
+ # The old normalize_artist_name function is replaced by _normalize_artist_name_for_db.
cookies.txt ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Netscape HTTP Cookie File
2
+ # http://curl.haxx.se/rfc/cookie_spec.html
3
+ # This is a generated file! Do not edit.
4
+
5
+ .google.co.in TRUE / FALSE 1779879930 SID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5gXFcXzKu0MEg459iyQC-DawACgYKAcsSAQASFQHGX2MicoGk2UVeoEwmgJXHlRhKgxoVAUF8yKoVnzc7eBa1FLcgaFI0GHEf0076
6
+ .google.co.in TRUE / TRUE 1779879930 __Secure-1PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g4c3ZxzKdEd1Mhrl0pZbOxgACgYKAVsSAQASFQHGX2MiavnS4hG6r9PIIcg6YbyNYhoVAUF8yKqXoLsOmRUH17LEFmBsJmj00076
7
+ .google.co.in TRUE / TRUE 1779879930 __Secure-3PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g2rUksuQKrmCvWTJl5MAO-AACgYKAXcSAQASFQHGX2MiFzJb37yrSKxsVt9TpyO3rxoVAUF8yKo4zfE-kGJQXrrqCvWt1F2z0076
8
+ .google.co.in TRUE / FALSE 1779879930 HSID AlrBbVr762FTKnR9r
9
+ .google.co.in TRUE / TRUE 1779879930 SSID AY9IxayqYVZvzmjbg
10
+ .google.co.in TRUE / FALSE 1779879930 APISID KJXTcPOVc1wynOkf/A0gq35uH7nqrD2sHt
11
+ .google.co.in TRUE / TRUE 1779879930 SAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
12
+ .google.co.in TRUE / TRUE 1779879930 __Secure-1PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
13
+ .google.co.in TRUE / TRUE 1779879930 __Secure-3PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
14
+ .youtube.com TRUE / FALSE 1779879930 SID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5gXFcXzKu0MEg459iyQC-DawACgYKAcsSAQASFQHGX2MicoGk2UVeoEwmgJXHlRhKgxoVAUF8yKoVnzc7eBa1FLcgaFI0GHEf0076
15
+ .youtube.com TRUE / TRUE 1779879930 __Secure-1PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g4c3ZxzKdEd1Mhrl0pZbOxgACgYKAVsSAQASFQHGX2MiavnS4hG6r9PIIcg6YbyNYhoVAUF8yKqXoLsOmRUH17LEFmBsJmj00076
16
+ .youtube.com TRUE / TRUE 1779879930 __Secure-3PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g2rUksuQKrmCvWTJl5MAO-AACgYKAXcSAQASFQHGX2MiFzJb37yrSKxsVt9TpyO3rxoVAUF8yKo4zfE-kGJQXrrqCvWt1F2z0076
17
+ .youtube.com TRUE / FALSE 1779879930 HSID AlrBbVr762FTKnR9r
18
+ .youtube.com TRUE / TRUE 1779879930 SSID AY9IxayqYVZvzmjbg
19
+ .youtube.com TRUE / FALSE 1779879930 APISID KJXTcPOVc1wynOkf/A0gq35uH7nqrD2sHt
20
+ .youtube.com TRUE / TRUE 1779879930 SAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
21
+ .youtube.com TRUE / TRUE 1779879930 __Secure-1PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
22
+ .youtube.com TRUE / TRUE 1779879930 __Secure-3PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
23
+ .google.com TRUE / FALSE 1779879930 SID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5gXFcXzKu0MEg459iyQC-DawACgYKAcsSAQASFQHGX2MicoGk2UVeoEwmgJXHlRhKgxoVAUF8yKoVnzc7eBa1FLcgaFI0GHEf0076
24
+ .google.com TRUE / TRUE 1779879930 __Secure-1PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g4c3ZxzKdEd1Mhrl0pZbOxgACgYKAVsSAQASFQHGX2MiavnS4hG6r9PIIcg6YbyNYhoVAUF8yKqXoLsOmRUH17LEFmBsJmj00076
25
+ .google.com TRUE / TRUE 1779879930 __Secure-3PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g2rUksuQKrmCvWTJl5MAO-AACgYKAXcSAQASFQHGX2MiFzJb37yrSKxsVt9TpyO3rxoVAUF8yKo4zfE-kGJQXrrqCvWt1F2z0076
26
+ accounts.google.com FALSE / TRUE 1779879930 LSID s.IN|s.youtube:g.a000wAhVqOTDpGgjDH7Pf7RrPNz3vr78_8PDeNlEceQlQkwmoVsxuirTUWRLoM00ID6trajSqwACgYKAUcSAQASFQHGX2MikQ3vP4aBJiiLODE67GvHjxoVAUF8yKrT6sNlhs8WpDEBrUgbV6Z40076
27
+ accounts.google.com FALSE / TRUE 1779879930 __Host-1PLSID s.IN|s.youtube:g.a000wAhVqOTDpGgjDH7Pf7RrPNz3vr78_8PDeNlEceQlQkwmoVsx5dHgA0ltXw5_s5JqpV33RwACgYKAcMSAQASFQHGX2MiIJQN39qYTrseTWPaWD4HsxoVAUF8yKoAYpD_vsYowAwfMU_XHyDP0076
28
+ accounts.google.com FALSE / TRUE 1779879930 __Host-3PLSID s.IN|s.youtube:g.a000wAhVqOTDpGgjDH7Pf7RrPNz3vr78_8PDeNlEceQlQkwmoVsxMrH4edmA0Sk2aCr_JB0otQACgYKAeESAQASFQHGX2Mi32yDXPduRs6va0rn6GC5RRoVAUF8yKqcf-6KcdPuo84mON0yJt1p0076
29
+ .google.com TRUE / FALSE 1779879930 HSID A9cYCuVB0B1JG0iMJ
30
+ .google.com TRUE / TRUE 1779879930 SSID AraWWWt-YGAi64b36
31
+ .google.com TRUE / FALSE 1779879930 APISID KJXTcPOVc1wynOkf/A0gq35uH7nqrD2sHt
32
+ .google.com TRUE / TRUE 1779879930 SAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
33
+ .google.com TRUE / TRUE 1779879930 __Secure-1PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
34
+ .google.com TRUE / TRUE 1779879930 __Secure-3PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
35
+ accounts.google.com FALSE / TRUE 1779879930 ACCOUNT_CHOOSER AFx_qI5oRXyf17qMjObhWuIY9AOE6A_Ray41SOxmJjGw55_5KkPUQ3souhtO66I_zBDHfXZuXm4EeEUcn3eeGosBfT4Am6QpVSBvZnB2yB9DHatwV0oACzTmQuml_6rbpXa17BAf_VC2a7hqgQFE7D2Baw34xchUmKKjlQyhC0L-yRbJ1nChSJE
36
+ accounts.google.com FALSE / TRUE 1779879930 __Host-GAPS 1:WoKDKuFo-QsuiQ__u8R0bZkDkxbSqulpCw5vNSpg3fmHBdyk0X5ujNBS7TpXxdLI12KBdsOr2h8Z-6WFwTsqWa0p2aF8Gw:lIEQiW_cN5Mfh7cg
37
+ .google.com TRUE / FALSE 1776855930 SIDCC AKEyXzWWLFgfGcR1SU0HwNgXYpQHe1o7AVeg52qxuxdGTDliAZigls_uAzC6r7-HnVCjn-oI9g
38
+ .google.com TRUE / TRUE 1776855930 __Secure-1PSIDCC AKEyXzWvF7I8iLHZYYq5QgsoAYj7wDxqfqiZ6FlUlhDs6P7LA5TZH7L3e_JfU0bCAktqWrgo
39
+ .youtube.com TRUE / TRUE 1779879931 LOGIN_INFO AFmmF2swRAIgLKv4yzMn5hGfO2gZ1Lamv-dxDvBepecZijwAZBhyG8oCIESJDPdHX4G8uw_zkBebNBVvSI6-G0CG2vdSisyM9vtr:QUQ3MjNmejd2REFSVHBFZ0pGaldxeHJWVFNvRTNhZElGMTVWQnd5VWZWZHpaSFZSamNoNXQ4TEZOeFI5TVQ0YmhGcHFCNkI4QmpkTUJmWjFSZEVLSWhHMUgzNlRqUU9Ec05wb2hXSnpKYkdtTUxXNUJoZ0c1QllPYWNUODlXZ1RRRzJJcmtaaFBqSmhFZkVvZlpMX05Pb3p3NW5RT1M2RjN3
40
+ .youtube.com TRUE / TRUE 1779879936 PREF tz=Asia.Calcutta&f6=40000000&f5=30000&f7=100
41
+ .doubleclick.net TRUE / TRUE 1747911943 ar_debug 1
42
+ .google.co.in TRUE / TRUE 1761131136 NID 523=StJhP2qgudrcu8th5bYdYg5cAeLjY_pXnR61hJWpd06GsdW7PYlGQNdLMLrlfuHsMBxxgIYhJA6RlQF5vFS3Csma_wMR8kryNxcd7u6aHxFEi32JSap9J91OxfLe_mBajIQ6wXuZdU5tYQ--dpkcY2FqmJg_y9x7XDWHCjNq_ewcXdDZLTCmLPp0tp6p__TVnw
43
+ .google.com TRUE / TRUE 1761131136 NID 523=cNJUeslcFkiCdmctX0xtiT4rEgPLlWSiEXOANvldzPK6e96DlqTao4wqmANuyDVQzLR1vJrklSIh1_j9DUF8hQ7o6k7Tqmo4QhyJpYSVXoiC44ZsDDDVkCpY9CGKwwBTztjT58U4md_2AffzbXv9Kt_CyDfFrnfofBen2K_ODXW6q9OLVCqsEZYJv8m_pkufM7YMHi2TEA
44
+ .youtube.com TRUE / TRUE 1776855938 __Secure-1PSIDTS sidts-CjEB7pHptYpr4cqqcAbZUIjsQ0RlTvrpGQreS89hEbVkB0gN2FyF_ucpeK5FcYmHAkpCEAA
45
+ .youtube.com TRUE / TRUE 1776855938 __Secure-3PSIDTS sidts-CjEB7pHptYpr4cqqcAbZUIjsQ0RlTvrpGQreS89hEbVkB0gN2FyF_ucpeK5FcYmHAkpCEAA
46
+ .doubleclick.net TRUE / TRUE 1779879943 IDE AHWqTUnpftQR_4NvHwvKKNhkspPHQ3TAjXc0XUD2FdbkAbnYeirHvkj82PxUO2p7
47
+ .youtube.com TRUE / FALSE 1776855943 SIDCC AKEyXzWaYoLGtPsJv15v2y6wmwYyb_oQkIsgS93G1nuiXbdK6aJPX1doI05KpiuFDzbEU_vlDg
48
+ .youtube.com TRUE / TRUE 1776855943 __Secure-1PSIDCC AKEyXzV6L6aWIN8bcBW0E_Rl5X8rY5tNpA-SUlskZVRav2gTzDXFE0_9XO9S8N6m8bJ-fc2BdA
49
+ .youtube.com TRUE / TRUE 1776855943 __Secure-3PSIDCC AKEyXzXlWdER_dMwpG7ZVhzcKGTJpdBvQihCJ74mSMpfcfc9MrB6uY7gCB_twFINxOaKjDkDwg
50
+ .google.com TRUE / TRUE 1776855944 __Secure-3PSIDCC AKEyXzUpShd5DkqaDdKE6nVbnrY7TF8yAqwfFdthjT_p6RUh3jaNeFRH0O4NCRbFSKHv3SG_KQ
51
+ .google.co.in TRUE / FALSE 1779879930 SID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5gXFcXzKu0MEg459iyQC-DawACgYKAcsSAQASFQHGX2MicoGk2UVeoEwmgJXHlRhKgxoVAUF8yKoVnzc7eBa1FLcgaFI0GHEf0076
52
+ .google.co.in TRUE / TRUE 1779879930 __Secure-1PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g4c3ZxzKdEd1Mhrl0pZbOxgACgYKAVsSAQASFQHGX2MiavnS4hG6r9PIIcg6YbyNYhoVAUF8yKqXoLsOmRUH17LEFmBsJmj00076
53
+ .google.co.in TRUE / TRUE 1779879930 __Secure-3PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g2rUksuQKrmCvWTJl5MAO-AACgYKAXcSAQASFQHGX2MiFzJb37yrSKxsVt9TpyO3rxoVAUF8yKo4zfE-kGJQXrrqCvWt1F2z0076
54
+ .google.co.in TRUE / FALSE 1779879930 HSID AlrBbVr762FTKnR9r
55
+ .google.co.in TRUE / TRUE 1779879930 SSID AY9IxayqYVZvzmjbg
56
+ .google.co.in TRUE / FALSE 1779879930 APISID KJXTcPOVc1wynOkf/A0gq35uH7nqrD2sHt
57
+ .google.co.in TRUE / TRUE 1779879930 SAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
58
+ .google.co.in TRUE / TRUE 1779879930 __Secure-1PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
59
+ .google.co.in TRUE / TRUE 1779879930 __Secure-3PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
60
+ .youtube.com TRUE / FALSE 1779879930 SID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5gXFcXzKu0MEg459iyQC-DawACgYKAcsSAQASFQHGX2MicoGk2UVeoEwmgJXHlRhKgxoVAUF8yKoVnzc7eBa1FLcgaFI0GHEf0076
61
+ .youtube.com TRUE / TRUE 1779879930 __Secure-1PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g4c3ZxzKdEd1Mhrl0pZbOxgACgYKAVsSAQASFQHGX2MiavnS4hG6r9PIIcg6YbyNYhoVAUF8yKqXoLsOmRUH17LEFmBsJmj00076
62
+ .youtube.com TRUE / TRUE 1779879930 __Secure-3PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g2rUksuQKrmCvWTJl5MAO-AACgYKAXcSAQASFQHGX2MiFzJb37yrSKxsVt9TpyO3rxoVAUF8yKo4zfE-kGJQXrrqCvWt1F2z0076
63
+ .youtube.com TRUE / FALSE 1779879930 HSID AlrBbVr762FTKnR9r
64
+ .youtube.com TRUE / TRUE 1779879930 SSID AY9IxayqYVZvzmjbg
65
+ .youtube.com TRUE / FALSE 1779879930 APISID KJXTcPOVc1wynOkf/A0gq35uH7nqrD2sHt
66
+ .youtube.com TRUE / TRUE 1779879930 SAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
67
+ .youtube.com TRUE / TRUE 1779879930 __Secure-1PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
68
+ .youtube.com TRUE / TRUE 1779879930 __Secure-3PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
69
+ .google.com TRUE / FALSE 1779879930 SID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5gXFcXzKu0MEg459iyQC-DawACgYKAcsSAQASFQHGX2MicoGk2UVeoEwmgJXHlRhKgxoVAUF8yKoVnzc7eBa1FLcgaFI0GHEf0076
70
+ .google.com TRUE / TRUE 1779879930 __Secure-1PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g4c3ZxzKdEd1Mhrl0pZbOxgACgYKAVsSAQASFQHGX2MiavnS4hG6r9PIIcg6YbyNYhoVAUF8yKqXoLsOmRUH17LEFmBsJmj00076
71
+ .google.com TRUE / TRUE 1779879930 __Secure-3PSID g.a000wAhVqLJmWACFacrDTfJh2zhItJRrqC0e6vkdPhdLTwsRAK5g2rUksuQKrmCvWTJl5MAO-AACgYKAXcSAQASFQHGX2MiFzJb37yrSKxsVt9TpyO3rxoVAUF8yKo4zfE-kGJQXrrqCvWt1F2z0076
72
+ accounts.google.com FALSE / TRUE 1779879930 LSID s.IN|s.youtube:g.a000wAhVqOTDpGgjDH7Pf7RrPNz3vr78_8PDeNlEceQlQkwmoVsxuirTUWRLoM00ID6trajSqwACgYKAUcSAQASFQHGX2MikQ3vP4aBJiiLODE67GvHjxoVAUF8yKrT6sNlhs8WpDEBrUgbV6Z40076
73
+ accounts.google.com FALSE / TRUE 1779879930 __Host-1PLSID s.IN|s.youtube:g.a000wAhVqOTDpGgjDH7Pf7RrPNz3vr78_8PDeNlEceQlQkwmoVsx5dHgA0ltXw5_s5JqpV33RwACgYKAcMSAQASFQHGX2MiIJQN39qYTrseTWPaWD4HsxoVAUF8yKoAYpD_vsYowAwfMU_XHyDP0076
74
+ accounts.google.com FALSE / TRUE 1779879930 __Host-3PLSID s.IN|s.youtube:g.a000wAhVqOTDpGgjDH7Pf7RrPNz3vr78_8PDeNlEceQlQkwmoVsxMrH4edmA0Sk2aCr_JB0otQACgYKAeESAQASFQHGX2Mi32yDXPduRs6va0rn6GC5RRoVAUF8yKqcf-6KcdPuo84mON0yJt1p0076
75
+ .google.com TRUE / FALSE 1779879930 HSID A9cYCuVB0B1JG0iMJ
76
+ .google.com TRUE / TRUE 1779879930 SSID AraWWWt-YGAi64b36
77
+ .google.com TRUE / FALSE 1779879930 APISID KJXTcPOVc1wynOkf/A0gq35uH7nqrD2sHt
78
+ .google.com TRUE / TRUE 1779879930 SAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
79
+ .google.com TRUE / TRUE 1779879930 __Secure-1PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
80
+ .google.com TRUE / TRUE 1779879930 __Secure-3PAPISID fnAV1XwDkm72vD1h/A-vR6EoZfDSqhRQh_
81
+ accounts.google.com FALSE / TRUE 1779879930 ACCOUNT_CHOOSER AFx_qI5oRXyf17qMjObhWuIY9AOE6A_Ray41SOxmJjGw55_5KkPUQ3souhtO66I_zBDHfXZuXm4EeEUcn3eeGosBfT4Am6QpVSBvZnB2yB9DHatwV0oACzTmQuml_6rbpXa17BAf_VC2a7hqgQFE7D2Baw34xchUmKKjlQyhC0L-yRbJ1nChSJE
82
+ accounts.google.com FALSE / TRUE 1779879930 __Host-GAPS 1:WoKDKuFo-QsuiQ__u8R0bZkDkxbSqulpCw5vNSpg3fmHBdyk0X5ujNBS7TpXxdLI12KBdsOr2h8Z-6WFwTsqWa0p2aF8Gw:lIEQiW_cN5Mfh7cg
83
+ .google.com TRUE / FALSE 1776855930 SIDCC AKEyXzWWLFgfGcR1SU0HwNgXYpQHe1o7AVeg52qxuxdGTDliAZigls_uAzC6r7-HnVCjn-oI9g
84
+ .google.com TRUE / TRUE 1776855930 __Secure-1PSIDCC AKEyXzWvF7I8iLHZYYq5QgsoAYj7wDxqfqiZ6FlUlhDs6P7LA5TZH7L3e_JfU0bCAktqWrgo
85
+ .youtube.com TRUE / TRUE 0 YSC Xbb5g3uswrw
86
+ .youtube.com TRUE / TRUE 1760871934 VISITOR_INFO1_LIVE aKJyb163PBU
87
+ .youtube.com TRUE / TRUE 1760871934 VISITOR_PRIVACY_METADATA CgJJThIEGgAgPg%3D%3D
88
+ .youtube.com TRUE / TRUE 1779879931 LOGIN_INFO AFmmF2swRAIgLKv4yzMn5hGfO2gZ1Lamv-dxDvBepecZijwAZBhyG8oCIESJDPdHX4G8uw_zkBebNBVvSI6-G0CG2vdSisyM9vtr:QUQ3MjNmejd2REFSVHBFZ0pGaldxeHJWVFNvRTNhZElGMTVWQnd5VWZWZHpaSFZSamNoNXQ4TEZOeFI5TVQ0YmhGcHFCNkI4QmpkTUJmWjFSZEVLSWhHMUgzNlRqUU9Ec05wb2hXSnpKYkdtTUxXNUJoZ0c1QllPYWNUODlXZ1RRRzJJcmtaaFBqSmhFZkVvZlpMX05Pb3p3NW5RT1M2RjN3
89
+ .youtube.com TRUE / TRUE 1760871933 __Secure-ROLLOUT_TOKEN CJ3xqffQi8myoAEQlrmttL_rjAMYyeCNtb_rjAM%3D
90
+ .youtube.com TRUE / TRUE 1779879936 PREF tz=Asia.Calcutta&f6=40000000&f5=30000&f7=100
91
+ .doubleclick.net TRUE / TRUE 1760871936 APC AfxxVi5Bfp_ZoNGyIgRSrHN1y2NRDiplBKstEvmbbCD3dRcnK6TFJA
92
+ .doubleclick.net TRUE / TRUE 1747911943 ar_debug 1
93
+ .doubleclick.net TRUE / TRUE 1760871936 receive-cookie-deprecation 1
94
+ .google.co.in TRUE / TRUE 1761131136 NID 523=StJhP2qgudrcu8th5bYdYg5cAeLjY_pXnR61hJWpd06GsdW7PYlGQNdLMLrlfuHsMBxxgIYhJA6RlQF5vFS3Csma_wMR8kryNxcd7u6aHxFEi32JSap9J91OxfLe_mBajIQ6wXuZdU5tYQ--dpkcY2FqmJg_y9x7XDWHCjNq_ewcXdDZLTCmLPp0tp6p__TVnw
95
+ .google.com TRUE / TRUE 1761131136 NID 523=cNJUeslcFkiCdmctX0xtiT4rEgPLlWSiEXOANvldzPK6e96DlqTao4wqmANuyDVQzLR1vJrklSIh1_j9DUF8hQ7o6k7Tqmo4QhyJpYSVXoiC44ZsDDDVkCpY9CGKwwBTztjT58U4md_2AffzbXv9Kt_CyDfFrnfofBen2K_ODXW6q9OLVCqsEZYJv8m_pkufM7YMHi2TEA
96
+ .youtube.com TRUE / TRUE 1776855938 __Secure-1PSIDTS sidts-CjEB7pHptYpr4cqqcAbZUIjsQ0RlTvrpGQreS89hEbVkB0gN2FyF_ucpeK5FcYmHAkpCEAA
97
+ .youtube.com TRUE / TRUE 1776855938 __Secure-3PSIDTS sidts-CjEB7pHptYpr4cqqcAbZUIjsQ0RlTvrpGQreS89hEbVkB0gN2FyF_ucpeK5FcYmHAkpCEAA
98
+ .doubleclick.net TRUE / TRUE 1779879943 IDE AHWqTUnpftQR_4NvHwvKKNhkspPHQ3TAjXc0XUD2FdbkAbnYeirHvkj82PxUO2p7
99
+ .youtube.com TRUE / FALSE 1776855943 SIDCC AKEyXzWaYoLGtPsJv15v2y6wmwYyb_oQkIsgS93G1nuiXbdK6aJPX1doI05KpiuFDzbEU_vlDg
100
+ .youtube.com TRUE / TRUE 1776855943 __Secure-1PSIDCC AKEyXzV6L6aWIN8bcBW0E_Rl5X8rY5tNpA-SUlskZVRav2gTzDXFE0_9XO9S8N6m8bJ-fc2BdA
101
+ .youtube.com TRUE / TRUE 1776855943 __Secure-3PSIDCC AKEyXzXlWdER_dMwpG7ZVhzcKGTJpdBvQihCJ74mSMpfcfc9MrB6uY7gCB_twFINxOaKjDkDwg
102
+ .google.com TRUE / TRUE 1776855944 __Secure-3PSIDCC AKEyXzUpShd5DkqaDdKE6nVbnrY7TF8yAqwfFdthjT_p6RUh3jaNeFRH0O4NCRbFSKHv3SG_KQ
settings.py ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+ from datetime import timedelta
4
+ from urllib.parse import urlparse
5
+ import nltk
6
+ from nltk.data import find
7
+ from os import getenv
8
+ from decouple import config
9
+
10
+ # def download_nltk_data():
11
+ # try:
12
+ # find('tokenizers/punkt')
13
+ # except LookupError:
14
+ # nltk.download('punkt')
15
+
16
+ # try:
17
+ # find('corpora/stopwords')
18
+ # except LookupError:
19
+ # nltk.download('stopwords')
20
+
21
+ # # Call the function to ensure the required NLTK data is available
22
+ # download_nltk_data()
23
+
24
+
25
+
26
+ # Replace the DATABASES section of your settings.py with this
27
+ tmpPostgres = urlparse(config("DATABASE_URL"))
28
+
29
+ DATABASES = {
30
+ 'default': {
31
+ 'ENGINE': 'django.db.backends.postgresql',
32
+ 'NAME': 'tunevault',
33
+ 'USER': 'tunevault_owner',
34
+ 'PASSWORD': config('DB_PASS'),
35
+ 'HOST': 'ep-patient-resonance-a19rwm71.ap-southeast-1.aws.neon.tech',
36
+ 'PORT': '5432',
37
+ 'OPTIONS': {
38
+ 'sslmode': 'require',
39
+ },
40
+ },
41
+ 'songs': {
42
+ 'ENGINE': 'django.db.backends.postgresql',
43
+ 'NAME': 'tunevault',
44
+ 'USER': 'tunevault_owner',
45
+ 'PASSWORD': config('DB_PASS'),
46
+ 'HOST': 'ep-patient-resonance-a19rwm71.ap-southeast-1.aws.neon.tech',
47
+ 'PORT': '5432',
48
+ 'OPTIONS': {
49
+ 'sslmode': 'require',
50
+ },
51
+ }
52
+ }
53
+
54
+
55
+ SPOTIFY_CLIENT_ID = config('SPOTIFY_CLIENT_ID')
56
+ SPOTIFY_CLIENT_SECRET = config('SPOTIFY_CLIENT_SECRET')
57
+ SPOTIFY_REDIRECT_URI = 'http://localhost:8000/spotify/callback'
58
+
59
+ # Google OAuth Settings
60
+ GOOGLE_CLIENT_ID = config('OAUTH_CLIENT_ID', default='')
61
+ GOOGLE_CLIENT_SECRET = config('OAUTH_CLIENT_SECRET', default='')
62
+ GOOGLE_REDIRECT_URI = config('GOOGLE_REDIRECT_URI', default='http://localhost:3000/auth/google/callback')
63
+
64
+
65
+ BASE_DIR = Path(__file__).resolve().parent.parent
66
+
67
+ SECRET_KEY = 'your-secret-key-here'
68
+
69
+ DEBUG = True
70
+
71
+ ALLOWED_HOSTS = [
72
+ 'songporter.onrender.com',
73
+ 'songporter.vercel.app',
74
+ 'localhost',
75
+ '127.0.0.1'
76
+ ]
77
+
78
+ INSTALLED_APPS = [
79
+ 'django.contrib.admin',
80
+ 'django.contrib.auth',
81
+ 'django.contrib.contenttypes',
82
+ 'django.contrib.sessions',
83
+ 'django.contrib.messages',
84
+ 'django.contrib.staticfiles',
85
+ 'django.contrib.sites',
86
+
87
+ # Third-party apps
88
+ 'rest_framework',
89
+ 'rest_framework.authtoken',
90
+ 'allauth',
91
+ 'allauth.account',
92
+ 'allauth.socialaccount',
93
+ 'allauth.socialaccount.providers.google',
94
+ 'youtube_dl',
95
+ 'corsheaders',
96
+ 'django_celery_results',
97
+ 'drf_yasg',
98
+
99
+ # Local apps
100
+ 'users',
101
+ 'songs',
102
+ ]
103
+
104
+ MIDDLEWARE = [
105
+ 'django.middleware.security.SecurityMiddleware',
106
+ 'whitenoise.middleware.WhiteNoiseMiddleware',
107
+ 'django.contrib.sessions.middleware.SessionMiddleware',
108
+ 'corsheaders.middleware.CorsMiddleware',
109
+ 'django.middleware.common.CommonMiddleware',
110
+ 'django.middleware.csrf.CsrfViewMiddleware',
111
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
112
+ 'django.contrib.messages.middleware.MessageMiddleware',
113
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
114
+ 'allauth.account.middleware.AccountMiddleware', # Add this line
115
+ ]
116
+
117
+ ROOT_URLCONF = 'tunevault.urls'
118
+
119
+ TEMPLATES = [
120
+ {
121
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
122
+ 'DIRS': [],
123
+ 'APP_DIRS': True,
124
+ 'OPTIONS': {
125
+ 'context_processors': [
126
+ 'django.template.context_processors.debug',
127
+ 'django.template.context_processors.request',
128
+ 'django.contrib.auth.context_processors.auth',
129
+ 'django.contrib.messages.context_processors.messages',
130
+ ],
131
+ },
132
+ },
133
+ ]
134
+
135
+ WSGI_APPLICATION = 'tunevault.wsgi.application'
136
+
137
+ AUTH_USER_MODEL = 'users.CustomUser'
138
+
139
+
140
+ AUTH_PASSWORD_VALIDATORS = [
141
+ {
142
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
143
+ },
144
+ {
145
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
146
+ },
147
+ {
148
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
149
+ },
150
+ {
151
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
152
+ },
153
+ ]
154
+
155
+ LANGUAGE_CODE = 'en-us'
156
+ TIME_ZONE = 'UTC'
157
+ USE_I18N = True
158
+ USE_TZ = True
159
+
160
+ STATIC_URL = '/static/'
161
+ # This production code might break development mode, so we check whether we're in DEBUG mode
162
+ if not DEBUG: # Tell Django to copy static assets into a path called `staticfiles` (this is specific to Render)
163
+ STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
164
+ # Enable the WhiteNoise storage backend, which compresses static files to reduce disk use
165
+ # and renames the files with unique names for each version to support long-term caching
166
+ STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
167
+ MEDIA_URL = '/media/'
168
+ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
169
+
170
+ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
171
+
172
+ # Django AllAuth settings
173
+ AUTHENTICATION_BACKENDS = [
174
+ 'django.contrib.auth.backends.ModelBackend',
175
+ 'allauth.account.auth_backends.AuthenticationBackend',
176
+ ]
177
+
178
+ SITE_ID = 1
179
+
180
+ SOCIALACCOUNT_PROVIDERS = {
181
+ 'google': {
182
+ 'APP': {
183
+ 'client_id': GOOGLE_CLIENT_ID,
184
+ 'secret': GOOGLE_CLIENT_SECRET,
185
+ 'key': ''
186
+ },
187
+ 'SCOPE': [
188
+ 'profile',
189
+ 'email',
190
+ ],
191
+ 'AUTH_PARAMS': {
192
+ 'access_type': 'online',
193
+ },
194
+ 'OAUTH_PKCE_ENABLED': True,
195
+ 'VERIFIED_EMAIL': True,
196
+ }
197
+ }
198
+
199
+ # Django allauth config
200
+ ACCOUNT_EMAIL_VERIFICATION = 'none' # Set to 'mandatory' in production
201
+ ACCOUNT_SIGNUP_FIELDS = ['email*', 'username*', 'password1*', 'password2*']
202
+ ACCOUNT_LOGIN_METHODS = {'email'}
203
+
204
+ # Login/logout URLs
205
+ LOGIN_REDIRECT_URL = '/'
206
+ LOGOUT_REDIRECT_URL = '/'
207
+
208
+ # Django Rest Framework settings
209
+ REST_FRAMEWORK = {
210
+ 'DEFAULT_AUTHENTICATION_CLASSES': [
211
+ 'rest_framework.authentication.TokenAuthentication',
212
+ 'rest_framework.authentication.SessionAuthentication',
213
+ ],
214
+ 'DEFAULT_PERMISSION_CLASSES': [
215
+ 'rest_framework.permissions.IsAuthenticated',
216
+ ],
217
+ 'DEFAULT_THROTTLE_CLASSES': [
218
+ 'rest_framework.throttling.AnonRateThrottle',
219
+ 'rest_framework.throttling.UserRateThrottle'
220
+ ],
221
+ 'DEFAULT_THROTTLE_RATES': {
222
+ 'anon': '100/day',
223
+ 'user': '1000/day'
224
+ },
225
+ 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
226
+ }
227
+
228
+ # Celery Configuration
229
+ CELERY_BROKER_URL = 'redis://default:Rh3mejxoZ1eRjMvtFnQWl79HezdWnKZV@redis-18712.crce182.ap-south-1-1.ec2.redns.redis-cloud.com:18712'
230
+ CELERY_RESULT_BACKEND = 'redis://default:Rh3mejxoZ1eRjMvtFnQWl79HezdWnKZV@redis-18712.crce182.ap-south-1-1.ec2.redns.redis-cloud.com:18712'
231
+ CELERY_ACCEPT_CONTENT = ['json']
232
+ CELERY_TASK_SERIALIZER = 'json'
233
+ CELERY_RESULT_SERIALIZER = 'json'
234
+ CELERY_TIMEZONE = TIME_ZONE
235
+ CELERY_TASK_TRACK_STARTED = True
236
+ CELERY_TASK_TIME_LIMIT = 30 * 60
237
+
238
+ # Define Celery Beat schedule
239
+ from celery.schedules import crontab
240
+
241
+ CELERY_BEAT_SCHEDULE = {
242
+ 'cleanup-cache-daily': {
243
+ 'task': 'songs.tasks.cleanup_cache',
244
+ 'schedule': crontab(hour=2, minute=0), # Run at 2:00 AM every day
245
+ 'options': {
246
+ 'expires': 3600, # Expires after 1 hour
247
+ },
248
+ },
249
+ }
250
+
251
+ # Redis Cache
252
+ CACHES = {
253
+ "default": {
254
+ "BACKEND": "django_redis.cache.RedisCache",
255
+ "LOCATION": "redis://default:Rh3mejxoZ1eRjMvtFnQWl79HezdWnKZV@redis-18712.crce182.ap-south-1-1.ec2.redns.redis-cloud.com:18712",
256
+ "OPTIONS": {
257
+ "CLIENT_CLASS": "django_redis.client.DefaultClient",
258
+ }
259
+ }
260
+ }
261
+
262
+
263
+ # Logging
264
+ LOGGING = {
265
+ 'version': 1,
266
+ 'disable_existing_loggers': False,
267
+ 'formatters': {
268
+ 'verbose': {
269
+ 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
270
+ 'style': '{',
271
+ },
272
+ 'simple': {
273
+ 'format': '{levelname} {message}',
274
+ 'style': '{',
275
+ },
276
+ },
277
+ 'handlers': {
278
+ 'file': {
279
+ 'level': 'DEBUG',
280
+ 'class': 'logging.FileHandler',
281
+ 'filename': 'debug.log',
282
+ 'formatter': 'verbose',
283
+ },
284
+ 'console': {
285
+ 'level': 'INFO',
286
+ 'class': 'logging.StreamHandler',
287
+ 'formatter': 'simple',
288
+ },
289
+ },
290
+ 'loggers': {
291
+ 'django': {
292
+ 'handlers': ['file', 'console'],
293
+ 'level': 'INFO',
294
+ 'propagate': True,
295
+ },
296
+ 'songs': {
297
+ 'handlers': ['file', 'console'],
298
+ 'level': 'DEBUG',
299
+ 'propagate': True,
300
+ },
301
+ },
302
+ }
303
+
304
+ # CORS settings
305
+ CORS_ALLOW_ALL_ORIGINS = True # For development only, restrict in production
306
+ CORS_EXPOSE_HEADERS = ['X-Thumbnail-URL', 'X-Song-Title', 'X-Song-Artist']
307
+
308
+ # Swagger settings
309
+ SWAGGER_SETTINGS = {
310
+ 'SECURITY_DEFINITIONS': {
311
+ 'Bearer': {
312
+ 'type': 'apiKey',
313
+ 'name': 'Authorization',
314
+ 'in': 'header'
315
+ }
316
+ },
317
+ 'USE_SESSION_AUTH': True,
318
+ 'DEFAULT_INFO': 'tunevault.urls.api_info',
319
+ 'OPERATIONS_SORTER': 'alpha',
320
+ 'TAGS_SORTER': 'alpha',
321
+ 'VALIDATOR_URL': None,
322
+ }
323
+
324
+ # Sentry configuration for error monitoring
325
+ import sentry_sdk
326
+ from sentry_sdk.integrations.django import DjangoIntegration
327
+ from sentry_sdk.integrations.celery import CeleryIntegration
328
+
329
+ sentry_sdk.init(
330
+ dsn="", # Add your Sentry DSN when deployed
331
+ integrations=[
332
+ DjangoIntegration(),
333
+ CeleryIntegration(),
334
+ ],
335
+ traces_sample_rate=0.1,
336
+ send_default_pii=True
337
+ )
338
+
339
+ # Rate limiting settings
340
+ RATELIMIT_USE_CACHE = 'default'
341
+ RATELIMIT_VIEW = 'songs.views.ratelimited_error'
342
+ RATELIMIT_ENABLE = True
343
+
344
+ # Format support
345
+ SUPPORTED_AUDIO_FORMATS = ['mp3', 'aac']
346
+ DEFAULT_AUDIO_FORMAT = 'mp3'
347
+
348
+ # Hugging Face Space URLs
349
+ HUGGINGFACE_RECOMMENDATION_URL = "https://monilm-songporter.hf.space/recommendations/"
350
+ HUGGINGFACE_ARTIST_INFO_URL = "https://monilm-songporter.hf.space/artist-info/"