import telebot import os import datetime import subprocess import requests from bs4 import BeautifulSoup import re # Replace 'YOUR_API_TOKEN' with your actual Telegram Bot API token bot = telebot.TeleBot("TOKEN") dldir = "/path/to/Music/" workingdir = "/path/to/script" script_dir = os.path.dirname(os.path.abspath(__file__)) spotdl_log_path = os.path.join(script_dir, "spotdl.log") with open(spotdl_log_path, "w") as _log_init: _log_init.write(f"{datetime.datetime.now().isoformat()} Starting new run\n") print("tg_spotdl bot is running.") @bot.message_handler(commands=["start"]) def start(message): bot.send_message( message.chat.id, "Hi! Send me a Spotify artist URL (not playlists, albums, or tracks).", ) bot.register_next_step_handler(message, process_url) print("User pushed start.") def resolve_spotify_link(url): """ Resolves Spotify short URLs (spotify.link and spotify.app.link) to their final destination. Returns the final resolved URL or the original URL if resolution fails. """ try: if re.match( r"^https?://(www\.)?(spotify\.link|spotify\.app\.link)/", url, flags=re.IGNORECASE, ): headers = {"User-Agent": "Mozilla/5.0"} response = requests.get( url, headers=headers, timeout=10, allow_redirects=True ) final_url = response.url print(f"Resolved short link: {url} -> {final_url}") if re.match( r"^https?://open\.spotify\.com/", final_url, flags=re.IGNORECASE ): return final_url html = "" try: html = response.text or "" except Exception: html = "" # Try meta refresh URL m = re.search( r']+http-equiv=["\']refresh["\'][^>]+content=["\'][^"\']*url=([^"\']+)["\']', html, flags=re.IGNORECASE, ) if m: candidate = m.group(1) if re.match( r"^https?://open\.spotify\.com/", candidate, flags=re.IGNORECASE ): return candidate # Try OpenGraph URL m = re.search( r']+property=["\']og:url["\'][^>]+content=["\']([^"\']+)["\']', html, flags=re.IGNORECASE, ) if m: candidate = m.group(1) if re.match( r"^https?://open\.spotify\.com/", candidate, flags=re.IGNORECASE ): return candidate # Try canonical link m = re.search( r']+rel=["\']canonical["\'][^>]+href=["\']([^"\']+)["\']', html, flags=re.IGNORECASE, ) if m: candidate = m.group(1) if re.match( r"^https?://open\.spotify\.com/", candidate, flags=re.IGNORECASE ): return candidate # Fallback: scan for artist URLs in the HTML m = re.search( r"https?://open\.spotify\.com/(?:intl-[a-z-]+/)?artist/[a-zA-Z0-9]+", html, flags=re.IGNORECASE, ) return m.group(0) if m else final_url return url except requests.RequestException as e: print(f"Error resolving Spotify short URL '{url}': {e}") return url def canonicalize_artist_url(url): """ Canonicalizes Spotify artist URLs by removing locale prefixes and query params. Example: https://open.spotify.com/intl-en/artist/?si=... -> https://open.spotify.com/artist/ """ m = re.match( r"^https?://open\.spotify\.com/(?:intl-[a-z-]+/)?(artist/[a-zA-Z0-9]+)(?:\?.*)?$", url, ) if m: canonical = "https://open.spotify.com/" + m.group(1) if canonical != url: print(f"Canonicalized artist URL: {url} -> {canonical}") return canonical return url def is_valid_artist_url(url): """Checks if the URL is a valid Spotify artist URL.""" # Allow locale-prefixed paths like /intl-en/artist/ and optional query params match = re.match( r"^https?://open\.spotify\.com/(?:intl-[a-z-]+/)?artist/([a-zA-Z0-9]+)(?:\?.*)?$", url, ) return bool(match) def get_artist_name(url): """Fetches the Spotify artist page and extracts the cleaned artist's name.""" headers = {"User-Agent": "Mozilla/5.0"} try: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() soup = BeautifulSoup(response.text, "html.parser") title_tag = soup.find("title") if title_tag: artist_name = title_tag.text.replace(" | Spotify", "").strip() # Remove unwanted suffixes that occasionally appear artist_name = re.sub( r"\s*(Songs and Music|Songs|Songs, Albums, Bio & More)\s*$", "", artist_name, flags=re.IGNORECASE, ) return artist_name.strip() or "Unknown Artist" else: return "Unknown Artist" except requests.RequestException as e: print(f"Error fetching artist name: {e}") return "Unknown Artist" def process_url(message): raw_url = (message.text or "").strip() if not raw_url: bot.send_message(message.chat.id, "Please send a valid Spotify artist URL.") print("Empty message text received.") return # 1) Resolve spotify.link short URLs resolved_url = resolve_spotify_link(raw_url) # 2) Canonicalize open.spotify.com artist URL format resolved_url = canonicalize_artist_url(resolved_url) # 3) Validate URL if not is_valid_artist_url(resolved_url): bot.send_message( message.chat.id, "Please send a valid Spotify artist URL, not a playlist, album, or track.", ) print(f"Rejected URL: {raw_url} -> {resolved_url}") # Debugging return artist = get_artist_name(resolved_url) bot.send_message( message.chat.id, f"Downloading music for {artist}. This may take a while." ) artist_directory = os.path.join(dldir, artist) os.makedirs(artist_directory, exist_ok=True) os.chdir(artist_directory) with open(spotdl_log_path, "a") as log_file: log_file.write( f"{datetime.datetime.now().isoformat()} Starting spotdl for '{artist}' url={resolved_url} dir={artist_directory}\n" ) subprocess.run( [ f"{workingdir}bin/spotdl", "--format", "opus", "--bitrate", "80k", resolved_url, ], stdout=log_file, stderr=log_file, ) log_file.write( f"{datetime.datetime.now().isoformat()} Finished spotdl for '{artist}'\n" ) bot.send_message(message.chat.id, f"Finished downloading {artist}.") print(f"Download completed for {artist}.") bot.send_message( message.chat.id, "Send another artist URL or use /start to begin again." ) @bot.message_handler( func=lambda m: isinstance(m.text, str) and re.match( r"^https?://open\.spotify\.com/(?:intl-[a-z-]+/)?artist/", m.text.strip() ) ) def process_direct_artist_url(message): process_url(message) @bot.message_handler( func=lambda m: isinstance(m.text, str) and re.match( r"^https?://(www\.)?(spotify\.link|spotify\.app\.link)/", m.text.strip() ) ) def process_spotify_link(message): process_url(message) @bot.message_handler(func=lambda message: True) def echo_all(message): bot.reply_to(message, "I don't understand. Please send a Spotify artist URL.") if __name__ == "__main__": bot.polling()