""" 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