Update: Fix empty album and iOS playback issues, add diverse home content
This commit is contained in:
parent
e1cb73f817
commit
e23714bbd6
5 changed files with 17909 additions and 13684 deletions
|
|
@ -130,9 +130,31 @@ async def get_playlist(id: str):
|
||||||
try:
|
try:
|
||||||
from ytmusicapi import YTMusic
|
from ytmusicapi import YTMusic
|
||||||
yt = YTMusic()
|
yt = YTMusic()
|
||||||
# ytmusicapi returns a dict with 'tracks' list
|
|
||||||
playlist_data = yt.get_playlist(id, limit=100)
|
|
||||||
|
|
||||||
|
playlist_data = None
|
||||||
|
is_album = False
|
||||||
|
|
||||||
|
# Try as Album first if ID looks like an album (MPREb...) or just try block
|
||||||
|
if id.startswith("MPREb"):
|
||||||
|
try:
|
||||||
|
playlist_data = yt.get_album(id)
|
||||||
|
is_album = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not playlist_data:
|
||||||
|
try:
|
||||||
|
# ytmusicapi returns a dict with 'tracks' list
|
||||||
|
playlist_data = yt.get_playlist(id, limit=100)
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback: Try as album if not tried yet
|
||||||
|
if not is_album:
|
||||||
|
try:
|
||||||
|
playlist_data = yt.get_album(id)
|
||||||
|
is_album = True
|
||||||
|
except:
|
||||||
|
raise e # Re-raise if both fail
|
||||||
|
|
||||||
# Format to match our app's Protocol
|
# Format to match our app's Protocol
|
||||||
formatted_tracks = []
|
formatted_tracks = []
|
||||||
if 'tracks' in playlist_data:
|
if 'tracks' in playlist_data:
|
||||||
|
|
@ -146,17 +168,22 @@ async def get_playlist(id: str):
|
||||||
|
|
||||||
# Safely extract thumbnails
|
# Safely extract thumbnails
|
||||||
thumbnails = track.get('thumbnails', [])
|
thumbnails = track.get('thumbnails', [])
|
||||||
|
if not thumbnails and is_album:
|
||||||
|
# Albums sometimes have thumbnails at root level, not per track
|
||||||
|
thumbnails = playlist_data.get('thumbnails', [])
|
||||||
|
|
||||||
cover_url = thumbnails[-1]['url'] if thumbnails else "https://placehold.co/300x300"
|
cover_url = thumbnails[-1]['url'] if thumbnails else "https://placehold.co/300x300"
|
||||||
|
|
||||||
# Safely extract album
|
# Safely extract album
|
||||||
album_info = track.get('album')
|
album_info = track.get('album')
|
||||||
album_name = album_info.get('name', 'Single') if album_info else "Single"
|
# If it's an album fetch, the album name is the playlist title
|
||||||
|
album_name = album_info.get('name', playlist_data.get('title')) if album_info else playlist_data.get('title', 'Single')
|
||||||
|
|
||||||
formatted_tracks.append({
|
formatted_tracks.append({
|
||||||
"title": track.get('title', 'Unknown Title'),
|
"title": track.get('title', 'Unknown Title'),
|
||||||
"artist": artist_names,
|
"artist": artist_names,
|
||||||
"album": album_name,
|
"album": album_name,
|
||||||
"duration": track.get('duration_seconds', 0),
|
"duration": track.get('duration_seconds', track.get('length_seconds', 0)),
|
||||||
"cover_url": cover_url,
|
"cover_url": cover_url,
|
||||||
"id": track.get('videoId'),
|
"id": track.get('videoId'),
|
||||||
"url": f"https://music.youtube.com/watch?v={track.get('videoId')}"
|
"url": f"https://music.youtube.com/watch?v={track.get('videoId')}"
|
||||||
|
|
@ -167,10 +194,10 @@ async def get_playlist(id: str):
|
||||||
p_cover = thumbnails[-1]['url'] if thumbnails else "https://placehold.co/300x300"
|
p_cover = thumbnails[-1]['url'] if thumbnails else "https://placehold.co/300x300"
|
||||||
|
|
||||||
formatted_playlist = {
|
formatted_playlist = {
|
||||||
"id": playlist_data.get('id'),
|
"id": playlist_data.get('browseId', playlist_data.get('id')),
|
||||||
"title": clean_title(playlist_data.get('title', 'Unknown')),
|
"title": clean_title(playlist_data.get('title', 'Unknown')),
|
||||||
"description": clean_description(playlist_data.get('description', '')),
|
"description": clean_description(playlist_data.get('description', '')),
|
||||||
"author": playlist_data.get('author', {}).get('name', 'YouTube Music'),
|
"author": playlist_data.get('author', {}).get('name', 'YouTube Music') if not is_album else ", ".join([a.get('name','') for a in playlist_data.get('artists', [])]),
|
||||||
"cover_url": p_cover,
|
"cover_url": p_cover,
|
||||||
"tracks": formatted_tracks
|
"tracks": formatted_tracks
|
||||||
}
|
}
|
||||||
|
|
@ -403,7 +430,7 @@ async def stream_audio(id: str):
|
||||||
print(f"DEBUG: Fetching new stream URL for '{id}'")
|
print(f"DEBUG: Fetching new stream URL for '{id}'")
|
||||||
url = f"https://www.youtube.com/watch?v={id}"
|
url = f"https://www.youtube.com/watch?v={id}"
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
'format': 'bestaudio/best',
|
'format': 'bestaudio[ext=m4a]/best[ext=mp4]/best', # Prefer m4a/aac for iOS
|
||||||
'quiet': True,
|
'quiet': True,
|
||||||
'noplaylist': True,
|
'noplaylist': True,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1095
backend/data.json
1095
backend/data.json
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
36
deploy_commands.sh
Executable file
36
deploy_commands.sh
Executable file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 1. Deployment to GitHub
|
||||||
|
echo "--- 🚀 Deploying to GitHub ---"
|
||||||
|
# Enter your correct repo URL here if different
|
||||||
|
REPO_URL="https://github.com/vndangkhoa/spotify-clone.git"
|
||||||
|
|
||||||
|
if git remote | grep -q "origin"; then
|
||||||
|
echo "Remote 'origin' already exists. Setting URL..."
|
||||||
|
git remote set-url origin $REPO_URL
|
||||||
|
else
|
||||||
|
git remote add origin $REPO_URL
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Staging and Committing changes..."
|
||||||
|
git add .
|
||||||
|
git commit -m "Update: Fix empty album and iOS playback issues, add diverse home content"
|
||||||
|
|
||||||
|
echo "Pushing code..."
|
||||||
|
# This might fail if the repo doesn't exist on GitHub yet.
|
||||||
|
# Go to https://github.com/new and create 'spotify-clone' first!
|
||||||
|
git push -u origin main
|
||||||
|
|
||||||
|
# 2. Deployment to Docker Hub
|
||||||
|
echo ""
|
||||||
|
echo "--- 🐳 Deploying to Docker Hub ---"
|
||||||
|
echo "Building Image..."
|
||||||
|
# Ensure Docker Desktop is running!
|
||||||
|
# Use --platform to build for Synology NAS (x86_64) from Apple Silicon Mac
|
||||||
|
docker build --platform linux/amd64 -t vndangkhoa/spotify-clone:latest .
|
||||||
|
|
||||||
|
echo "Pushing Image..."
|
||||||
|
docker push vndangkhoa/spotify-clone:latest
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- ✅ Deployment Script Finished ---"
|
||||||
105
fetch_data.py
105
fetch_data.py
|
|
@ -1,40 +1,64 @@
|
||||||
from ytmusicapi import YTMusic
|
from ytmusicapi import YTMusic
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
yt = YTMusic()
|
yt = YTMusic()
|
||||||
|
|
||||||
|
# Define diverse categories to fetch
|
||||||
CATEGORIES = {
|
CATEGORIES = {
|
||||||
"Trending Vietnam": "Top 50 Vietnam",
|
"Trending Vietnam": {"query": "Top 50 Vietnam", "type": "playlists"},
|
||||||
"Vietnamese Artists": "Vietnamese Pop Hits",
|
"Global Hits": {"query": "Global Top 50", "type": "playlists"},
|
||||||
"Ballad Singers": "Vietnamese Ballad",
|
"New Albums 2024": {"query": "New Albums 2024 Vietnam", "type": "albums"},
|
||||||
"DJ & Remix": "Vinahouse Remix Vietnam",
|
"Chill Vibes": {"query": "Chill Lofi", "type": "playlists"},
|
||||||
"YouTube Stars": "Vietnamese Cover Songs"
|
"Party Time": {"query": "Party EDM Hits", "type": "playlists"},
|
||||||
|
"Best of Ballad": {"query": "Vietnamese Ballad", "type": "playlists"},
|
||||||
|
"Hip Hop & Rap": {"query": "Vietnamese Rap", "type": "playlists"},
|
||||||
}
|
}
|
||||||
|
|
||||||
browse_data = {}
|
browse_data = {}
|
||||||
|
|
||||||
print("Starting data fetch...")
|
print("Starting diverse data fetch...")
|
||||||
|
|
||||||
for category, query in CATEGORIES.items():
|
def get_thumbnail(thumbnails):
|
||||||
print(f"\n--- Fetching Category: {category} (Query: '{query}') ---")
|
if not thumbnails:
|
||||||
|
return "https://placehold.co/300x300"
|
||||||
|
return thumbnails[-1]['url']
|
||||||
|
|
||||||
|
for category_name, info in CATEGORIES.items():
|
||||||
|
query = info["query"]
|
||||||
|
search_type = info["type"]
|
||||||
|
print(f"\n--- Fetching Category: {category_name} (Query: '{query}', Type: {search_type}) ---")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = yt.search(query, filter="playlists", limit=5)
|
results = yt.search(query, filter=search_type, limit=5)
|
||||||
|
|
||||||
category_playlists = []
|
category_items = []
|
||||||
|
|
||||||
for p_result in results[:4]: # Limit to 4 playlists per category
|
for result in results[:4]: # Limit to 4 items per category
|
||||||
playlist_id = p_result['browseId']
|
item_id = result['browseId']
|
||||||
print(f" > Processing: {p_result['title']}")
|
title = result['title']
|
||||||
|
print(f" > Processing: {title}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Fetch full playlist details
|
# Fetch details based on type
|
||||||
playlist_data = yt.get_playlist(playlist_id, limit=50)
|
if search_type == "albums":
|
||||||
|
# Use get_album
|
||||||
|
details = yt.get_album(item_id)
|
||||||
|
tracks_source = details.get('tracks', [])
|
||||||
|
is_album = True
|
||||||
|
description = f"Album by {', '.join([a.get('name') for a in details.get('artists', [])])} • {details.get('year')}"
|
||||||
|
else:
|
||||||
|
# Use get_playlist
|
||||||
|
details = yt.get_playlist(item_id, limit=50)
|
||||||
|
tracks_source = details.get('tracks', [])
|
||||||
|
is_album = False
|
||||||
|
description = details.get('description', '')
|
||||||
|
|
||||||
# Process Tracks
|
# Process Tracks
|
||||||
output_tracks = []
|
output_tracks = []
|
||||||
for track in playlist_data.get('tracks', []):
|
for track in tracks_source:
|
||||||
artists_list = track.get('artists') or []
|
artists_list = track.get('artists') or []
|
||||||
if isinstance(artists_list, list):
|
if isinstance(artists_list, list):
|
||||||
artists = ", ".join([a.get('name', 'Unknown') for a in artists_list])
|
artists = ", ".join([a.get('name', 'Unknown') for a in artists_list])
|
||||||
|
|
@ -42,42 +66,53 @@ for category, query in CATEGORIES.items():
|
||||||
artists = "Unknown Artist"
|
artists = "Unknown Artist"
|
||||||
|
|
||||||
thumbnails = track.get('thumbnails', [])
|
thumbnails = track.get('thumbnails', [])
|
||||||
cover_url = thumbnails[-1]['url'] if thumbnails else "https://placehold.co/300x300"
|
# Fallback for album tracks which might not have thumbnails
|
||||||
|
if not thumbnails and is_album:
|
||||||
|
thumbnails = details.get('thumbnails', [])
|
||||||
|
|
||||||
|
cover_url = get_thumbnail(thumbnails)
|
||||||
|
|
||||||
album_info = track.get('album')
|
album_info = track.get('album')
|
||||||
album_name = album_info.get('name', 'Single') if album_info else "Single"
|
# Use playlist/album title as album name if missing
|
||||||
|
album_name = album_info.get('name', title) if album_info else title
|
||||||
|
|
||||||
|
# Track ID can be missing in some album views (very rare)
|
||||||
|
track_id = track.get('videoId')
|
||||||
|
if not track_id: continue
|
||||||
|
|
||||||
output_tracks.append({
|
output_tracks.append({
|
||||||
"title": track.get('title', 'Unknown Title'),
|
"title": track.get('title', 'Unknown Title'),
|
||||||
"artist": artists,
|
"artist": artists,
|
||||||
"album": album_name,
|
"album": album_name,
|
||||||
"duration": track.get('duration_seconds', 0),
|
"duration": track.get('duration_seconds', track.get('length_seconds', 0)),
|
||||||
"cover_url": cover_url,
|
"cover_url": cover_url,
|
||||||
"id": track.get('videoId', 'unknown'),
|
"id": track_id,
|
||||||
"url": f"https://music.youtube.com/watch?v={track.get('videoId', '')}"
|
"url": f"https://music.youtube.com/watch?v={track_id}"
|
||||||
})
|
})
|
||||||
|
|
||||||
# Process Playlist Info
|
if not output_tracks:
|
||||||
p_thumbnails = playlist_data.get('thumbnails', [])
|
print(f" Skipping empty item: {title}")
|
||||||
p_cover = p_thumbnails[-1]['url'] if p_thumbnails else "https://placehold.co/300x300"
|
continue
|
||||||
|
|
||||||
category_playlists.append({
|
# Final Item Object
|
||||||
"id": playlist_data.get('id'),
|
category_items.append({
|
||||||
"title": playlist_data.get('title'),
|
"id": item_id,
|
||||||
"description": playlist_data.get('description', '') or f"Best of {category}",
|
"title": title,
|
||||||
"cover_url": p_cover,
|
"description": description or f"Best of {category_name}",
|
||||||
"tracks": output_tracks
|
"cover_url": get_thumbnail(details.get('thumbnails', result.get('thumbnails'))),
|
||||||
|
"tracks": output_tracks,
|
||||||
|
"type": "album" if is_album else "playlist"
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" Error processing playlist {playlist_id}: {e}")
|
print(f" Error processing {item_id}: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if category_playlists:
|
if category_items:
|
||||||
browse_data[category] = category_playlists
|
browse_data[category_name] = category_items
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error searching category {category}: {e}")
|
print(f"Error searching category {category_name}: {e}")
|
||||||
|
|
||||||
# Save to backend/data/browse_playlists.json
|
# Save to backend/data/browse_playlists.json
|
||||||
output_path = Path("backend/data/browse_playlists.json")
|
output_path = Path("backend/data/browse_playlists.json")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue