Spaces:
Running
Running
HF Space Fix#8
Browse files- app.py +299 -92
- artist_utils.py +307 -157
- cookies.txt +102 -0
- 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 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
26 |
-
version="1.
|
27 |
)
|
28 |
|
29 |
# Add CORS middleware
|
@@ -37,37 +65,103 @@ app.add_middleware(
|
|
37 |
|
38 |
# --- Initialize Recommender ---
|
39 |
recommender = None
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
|
|
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")
|
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
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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
|
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':
|
179 |
-
'artist_img':
|
180 |
-
'country':
|
181 |
-
'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 |
-
|
189 |
-
|
|
|
190 |
"""
|
191 |
-
|
192 |
-
Format matches the single artist endpoint with values directly in braces, not as key-value pairs.
|
193 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
194 |
try:
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
'
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
except Exception as e:
|
213 |
-
|
214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
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 |
-
# ---
|
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
|
2 |
-
import
|
3 |
import os
|
4 |
import re
|
5 |
|
6 |
-
#
|
7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
13 |
|
14 |
-
def
|
15 |
-
"""
|
16 |
-
global ARTIST_DATA, ARTIST_MAP
|
17 |
-
|
18 |
try:
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
-
|
|
|
|
|
|
|
|
|
33 |
try:
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
|
92 |
def get_artist_info(artist_name):
|
93 |
"""
|
94 |
-
Get artist information from the
|
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 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
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':
|
134 |
-
'artist_img':
|
135 |
-
'country':
|
136 |
-
'artist_genre':
|
137 |
}
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
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 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
#
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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/"
|