Files
groovy-zilean/cogs/music/translate.py

212 lines
6.2 KiB
Python

"""
URL and search query handling for Groovy-Zilean
Translates YouTube, Spotify, SoundCloud URLs and search queries into playable audio
"""
from typing import Any
import yt_dlp as ytdlp
import spotipy
# Updated yt-dlp options to handle current YouTube changes
ydl_opts = {
'format': 'bestaudio/best',
'quiet': True,
'no_warnings': False, # Show warnings for debugging
'default_search': 'ytsearch',
'ignoreerrors': True,
'source_address': '0.0.0.0', # Bind to IPv4 to avoid IPv6 issues
'extract_flat': False,
'nocheckcertificate': True,
# Add extractor args to handle YouTube's new requirements
'extractor_args': {
'youtube': {
'player_skip': ['webpage', 'configs'],
'player_client': ['android', 'web'],
}
},
}
async def main(url: str, sp: spotipy.Spotify) -> list[dict[str, Any] | str]:
#url = url.lower()
# Check if link or search
if not url.startswith("https://") and not url.startswith("http://"):
return await search_song(url)
#TODO add better regex or something
if 'spotify' in url:
if 'track' in url:
return await spotify_song(url, sp)
elif 'playlist' in url:
return await spotify_playlist(url, sp)
soundcloud_song = 'soundcloud' in url and 'sets' not in url
# Not implemented yet
# soundcloud_playlist = 'soundcloud' in url and 'sets' in url
youtube_song = 'watch?v=' in url or 'youtu.be/' in url
youtube_playlist = 'playlist?list=' in url
# Check for direct audio/video file URLs
# Supported formats: mp3, mp4, wav, ogg, flac, m4a, webm, aac, opus
audio_extensions = ('.mp3', '.mp4', '.wav', '.ogg', '.flac', '.m4a', '.webm', '.aac', '.opus')
is_direct_file = any(url.lower().endswith(ext) for ext in audio_extensions)
# Also check for URLs with query parameters (e.g., file.mp3?download=true)
is_direct_file = is_direct_file or any(ext in url.lower() for ext in audio_extensions)
if soundcloud_song or youtube_song or is_direct_file:
return await song_download(url)
if youtube_playlist:
return await playlist_download(url)
return []
async def search_song(search: str) -> list[dict[str, Any]]:
with ytdlp.YoutubeDL(ydl_opts) as ydl:
try:
info = ydl.extract_info(f"ytsearch1:{search}", download=False)
except Exception as e:
print(f"Error searching for '{search}': {e}")
return []
if info is None:
return []
if 'entries' not in info or len(info['entries']) == 0:
return []
info = info['entries'][0] # Get first search result
# Get the best audio stream URL
if 'url' not in info:
print(f"No URL found for: {search}")
return []
data = {
'url': info['url'],
'title': info.get('title', 'Unknown'),
'thumbnail': info.get('thumbnail', ''),
'duration': info.get('duration', 0)
}
return [data]
async def spotify_song(url: str, sp: spotipy.Spotify) -> list[dict[str, Any]]:
track = sp.track(url.split("/")[-1].split("?")[0])
search = ""
for i in track["artists"]:
# grabs all the artists name's if there's more than one
search = search + (i['name'] + ", ")
# remove last comma
search = search[:-2]
# set search to name
query = search + " - " + track['name']
return await search_song(query)
async def spotify_playlist(url: str, sp: spotipy.Spotify) -> list[str | dict[str, Any]]:
"""
Get songs from a Spotify playlist
Returns a mixed list where first item is dict, rest are search strings
"""
# Get the playlist uri code
code = url.split("/")[-1].split("?")[0]
# Grab the tracks if the playlist is correct
try:
results = sp.playlist_tracks(code)['items']
except spotipy.exceptions.SpotifyException:
return []
# Go through the tracks and build search queries
songs: list[str | dict[str, Any]] = [] # Explicit type for mypy
for track in results:
search = ""
# Fetch all artists
for artist in track['track']['artists']:
# Add all artists to search
search += f"{artist['name']}, "
# Remove last comma
search = search[:-2]
search += f" - {track['track']['name']}"
songs.append(search)
# Fetch first song's full data
while True:
search_result = await search_song(songs[0]) # type: ignore
if search_result == []:
songs.pop(0)
continue
else:
songs[0] = search_result[0] # Replace string with dict
break
return songs
async def song_download(url: str) -> list[dict[str, Any]]:
with ytdlp.YoutubeDL(ydl_opts) as ydl:
try:
info = ydl.extract_info(url, download=False)
except Exception as e:
print(f"Error downloading '{url}': {e}")
return []
if info is None:
return []
# Handle both direct videos and playlists with single entry
if 'entries' in info:
if len(info['entries']) == 0:
return []
info = info['entries'][0]
if 'url' not in info:
print(f"No URL found for: {url}")
return []
data = {
'url': info['url'],
'title': info.get('title', 'Unknown'),
'thumbnail': info.get('thumbnail', ''),
'duration': info.get('duration', 0)
}
return [data]
async def playlist_download(url: str) -> list[dict[str, Any]]:
with ytdlp.YoutubeDL(ydl_opts) as ydl:
try:
info = ydl.extract_info(url, download=False)
except Exception as e:
print(f"Error downloading playlist '{url}': {e}")
return []
if info is None:
return []
info = info['entries'] # Grabbing all songs in playlist
urls = []
for song in info:
if song is None or 'url' not in song:
continue
data = {
'url': song['url'],
'title': song.get('title', 'Unknown'),
'thumbnail': song.get('thumbnail', ''),
'duration': song.get('duration', 0)
}
urls.append(data)
return urls