import discord from discord.ext import commands from discord import app_commands import os import json import asyncio from datetime import datetime from dotenv import load_dotenv from typing import Optional import subprocess # Load environment variables load_dotenv() # Bot configuration intents = discord.Intents.default() intents.message_content = True intents.members = True class DownloadBot(commands.Bot): def __init__(self): super().__init__( command_prefix='!', intents=intents, help_command=None ) # Initialize data storage (in-memory for this example) # In production, you'd want to use a database self.download_queue = [] self.download_history = [] self.authorized_users = [] # Load data from files if they exist self.load_data() def load_data(self): """Load persistent bot_logs from JSON files""" try: if os.path.exists('bot_logs/download_history.json'): with open('bot_logs/download_history.json', 'r') as f: self.download_history = json.load(f) if os.path.exists('bot_logs/authorized_users.json'): with open('bot_logs/authorized_users.json', 'r') as f: self.authorized_users = set(json.load(f)) except Exception as e: print(f"Error loading data: {e}") def save_data(self): """Save persistent data to JSON files""" try: os.makedirs('bot_logs', exist_ok=True) with open('bot_logs/download_history.json', 'w') as f: json.dump(self.download_history, f, indent=2) with open('bot_logs/authorized_users.json', 'w') as f: json.dump(list(self.authorized_users), f, indent=2) except Exception as e: print(f"Error saving bot_logs: {e}") async def setup_hook(self): """Called when the bot is starting up""" print(f"Logged in as {self.user} (ID: {self.user.id})") print("------") # Sync slash commands try: synced = await self.tree.sync() print(f"Synced {len(synced)} command(s)") # threading.Thread(target=vt_worker).start() # Start the download worker in the background except Exception as e: print(f"Failed to sync commands: {e}") bot = DownloadBot() # Helper function to check if user is authorized def is_authorized(): def predicate(interaction: discord.Interaction): return interaction.user.id in bot.authorized_users or interaction.user.guild_permissions.administrator return app_commands.check(predicate) @bot.event async def on_ready(): print(f'{bot.user} has connected to Discord!') activity = discord.Game(name="Managing downloads | /help") await bot.change_presence(activity=activity) asyncio.create_task(usk_worker()) # /download command @bot.tree.command(name="download", description="Download a file from a URL|ID") @app_commands.describe( service="Service to use for downloading (e.g., AMZN, NF, HS, VIU, TID, MMAX, BLBL)", url="The URL|ID to download from", keys="Get keys only (default: False, True to get keys only)", quality="Desired video quality (default: 1080)", codec="Video codec to use (default: h265)", range_="Dynamic range to use (default: SDR)", bitrate="Video bitrate to use (default: Max)", start_season="Season to download (optional, e.g., 1)", start_episode="Specific episodes to download (e.g., 1)", end_season="Season to download (optional, e.g., 2)", end_episode="Specific episodes to download (e.g., 2)", video_language="Video language(s) to use (default: orig)", audio_language="Audio language(s) to use (default: orig,th)", subtitle_language="Subtitle language(s) to use (default: th,en)", audio_channel="Audio channel(s) to use (default: 2.0,5.1,Best)", worst="Download worst quality available (default: False, True to download worst)", proxy="Proxy to use (optional, e.g., http://username:password@proxyserver:port or nordvpn country code/id)", ### Unshackle options no_cache="Disable vault cache (default: False, True to disable cache)", ## for iTunes store_front="For iTunes: Store front to use (default: 143475)", ### for BiliBili or Other season="For BiliBili: Season to download (optional, e.g., 1)", title_language="For BiliBili | Laftel: Title language(s) to use (default: ja)", original_url="For BiliBili: Original URL to download from (optional, e.g., https://www.bilibili.com/video/BV1xxxxxx)", original_language="For BiliBili: Original language(s) to use (default: ja)", movie="For BiliBili | Laftel: Is this a movie? (default: False, True for movies, False for series)", # New parameter to indicate if it's a movie ) @app_commands.choices(keys=[ app_commands.Choice(name="True", value='True'), app_commands.Choice(name="False", value='False'), ]) @app_commands.choices(service=[ app_commands.Choice(name="Amazon Prime", value="AMZN"), app_commands.Choice(name="Netflix", value="NF"), app_commands.Choice(name="Hotstar", value="HS"), app_commands.Choice(name="VIU", value="VIU"), app_commands.Choice(name="TrueID", value="TID"), app_commands.Choice(name="Mono Max", value="MMAX"), app_commands.Choice(name="BiliBili", value="BLBL"), app_commands.Choice(name="FutureSkill", value="FSK"), app_commands.Choice(name="HBO Max", value="HMAX"), app_commands.Choice(name="iQIYI", value="IQ"), app_commands.Choice(name="WeTV", value="WTV"), app_commands.Choice(name="Crunchyroll", value="CR"), app_commands.Choice(name="Laftel", value="LT"), app_commands.Choice(name="Flixer", value="FLX"), app_commands.Choice(name="iTune", value="IT"), app_commands.Choice(name="Apple TV+", value="ATVP"), app_commands.Choice(name="TrueVisionNow", value="TVN"), app_commands.Choice(name="OneD", value="OND"), app_commands.Choice(name="HIDIVE", value="HIDI"), ]) # @app_commands.choices(quality=[ # app_commands.Choice(name="2160p", value="2160"), # app_commands.Choice(name="1440p", value="1440"), # app_commands.Choice(name="1080p", value="1080"), # app_commands.Choice(name="720p", value="720"), # app_commands.Choice(name="480p", value="480"), # app_commands.Choice(name="Best", value="Best"), # ]) @app_commands.choices(codec=[ app_commands.Choice(name="H264", value="H.264"), app_commands.Choice(name="H265", value="H.265"), app_commands.Choice(name="AV1", value="AV1"), app_commands.Choice(name="VP9", value="VP9"), ]) @app_commands.choices(range_=[ app_commands.Choice(name="HDR", value="HDR"), app_commands.Choice(name="SDR", value="SDR"), app_commands.Choice(name="DV", value="DV"), app_commands.Choice(name="DV+HDR", value="DV+HDR"), ]) @app_commands.choices(audio_channel=[ app_commands.Choice(name="2.0", value="2.0"), app_commands.Choice(name="5.1", value="5.1"), app_commands.Choice(name="Best", value= "Best"), ]) @app_commands.choices(worst=[ app_commands.Choice(name="True", value='True'), app_commands.Choice(name="False", value='False'), ]) @app_commands.choices(movie=[ app_commands.Choice(name="True", value='True'), app_commands.Choice(name="False", value='False'), ]) @app_commands.choices(no_cache=[ app_commands.Choice(name="True", value=1), app_commands.Choice(name="False", value=0), ]) async def download_command( interaction: discord.Interaction, service: str, url: str, keys: Optional[str] = 'False', quality: Optional[str] = '1080', codec: Optional[str] = "h.265", range_: Optional[str] = "SDR", bitrate: Optional[str] = "Max", start_season: Optional[int] = None, start_episode: Optional[int] = None, end_season: Optional[int] = None, end_episode: Optional[int] = None, video_language: Optional[str] = "all", audio_language: Optional[str] = "orig,th", subtitle_language: Optional[str] = "th,en", audio_channel: Optional[str] = "Best", worst: Optional[str] = 'False', proxy: Optional[str] = None, no_cache: Optional[int] = 0, # 1 for True and 0 for False # title_cache: Optional[int] = 0, # iTunes specific parameters store_front: Optional[str] = "143475", # BiliBili specific parameters season: Optional[int] = None, title_language: Optional[str] = "ja", original_url: Optional[str] = None, original_language: Optional[str] = "ja", movie: Optional[str] = 'False', ): # Check if user has permission if not (interaction.user.id in bot.authorized_users or interaction.user.guild_permissions.administrator): embed = discord.Embed( title="❌ Access Denied", description="You don't have permission to use this command.", color=0xff0000 ) await interaction.response.send_message(embed=embed, ephemeral=True) return try: bitrate = int(bitrate) except ValueError: if bitrate.lower() == 'max': bitrate = None else: embed = discord.Embed( title="❌ Invalid Bitrate", description="Bitrate must be an integer or 'Max'.", color=0xff0000 ) await interaction.response.send_message(embed=embed, ephemeral=True) return # Create download entry download_id = len(bot.download_history) + 1 download_entry = { 'interaction': interaction, 'data': { 'id': download_id, 'url': url, 'user': interaction.user.display_name, 'user_id': interaction.user.id, 'channel_id': interaction.channel_id, 'timestamp': datetime.now().isoformat(), 'status': 'queued', 'service': service.upper(), 'keys': keys == 'True', # Convert to boolean 'quality': quality.upper() if quality else None, 'codec': codec.upper() if codec else None, 'range': range_.upper() if range_ else None, 'bitrate': bitrate, 'start_season': f'{start_season:02}' if start_season is not None else None, 'start_episode': f'{start_episode:02}' if start_episode is not None else None, 'end_season': f'{end_season:02}' if end_season is not None else None, 'end_episode': f'{end_episode:02}' if end_episode is not None else None, 'video_language': video_language.lower() if video_language else None, 'audio_language': audio_language.lower() if audio_language else None, 'subtitle_language': subtitle_language.lower() if subtitle_language else None, 'audio_channel': audio_channel if audio_channel != "Best" else None, 'worst': worst == 'True', # Convert to boolean 'no_cache': no_cache == 1, # Convert to boolean # 'title_cache': title_cache == 1, 'proxy': proxy, ### iTunes specific parameters 'store_front': store_front if store_front else "143475", ### BiliBili specific parameters 'season': season, 'title_language': title_language.lower() if title_language else None, 'original_url': original_url, 'original_language': original_language.lower() if original_language else None, 'movie': movie if movie is not None else 'False', } } embed = discord.Embed( title="📥 Download Queued", description="Download request has been added to the queue.", color=0x00ff00 ) embed.add_field(name="🛠 Service", value=service, inline=True) embed.add_field(name="🆔 Download ID", value=download_id, inline=True) embed.add_field(name="🔗 URL", value=url, inline=False) embed.add_field(name="🔑 Keys", value=keys, inline=True) embed.add_field(name="🎥 Quality", value=quality, inline=True) embed.add_field(name="🎞 Codec", value=codec, inline=True) embed.add_field(name="🌈 Range", value=range_, inline=True) embed.add_field(name="🎥 Bitrate", value=bitrate if bitrate else "Max", inline=True) embed.add_field(name="🎯 Start Season", value=start_season or "None", inline=True) embed.add_field(name="🎯 End Season", value=end_season or "None", inline=True) embed.add_field(name="📺 Start Episode", value=start_episode or "None", inline=True) embed.add_field(name="📺 End Episode", value=end_episode or "None", inline=True) embed.add_field(name="🔊 Audio Language", value=audio_language, inline=False) embed.add_field(name="📜 Subtitle Language", value=subtitle_language, inline=False) embed.add_field(name="🔊 Audio Channel", value=audio_channel, inline=True) embed.add_field(name="📊 Queue Position", value=len(bot.download_queue), inline=False) embed.set_footer(text=f"Requested by {interaction.user.display_name}") await interaction.response.send_message(embed=embed) # Add to queue and history bot.download_queue.append(download_entry) bot.download_history.append(download_entry['data']) bot.save_data() async def usk_worker(): """Continuously process the download queue in the background""" while True: if bot.download_queue: entry = bot.download_queue.pop(0) await process_download(entry['data']) else: await asyncio.sleep(5) # Sleep briefly if queue is empty async def process_download(entry): """Background worker to process download queue""" entry['status'] = 'in_progress' bot.save_data() channel = bot.get_channel(entry['channel_id']) cmd=['/root/unshackle-SeFree/.venv/bin/unshackle','dl'] if entry['proxy'] and entry['service'] not in ['HIDI']: cmd += ['--proxy', entry['proxy']] elif entry['service'] in ['HIDI']: cmd += ['--proxy',"ca"] if entry['keys']: cmd.append('--skip-dl') # if entry['service'] in ['AMZN'] and not entry['keys']: # cmd += ['--delay', '30'] # elif entry['service'] in ['CR'] and not entry['keys']: # cmd += ['--delay', '15'] # elif entry['keys']: # cmd += ['--delay', '3'] # else: # cmd += ['--delay', '10'] if entry['no_cache'] or entry['service'] in ['HMAX'] : cmd.append('--no-cache') if entry['quality'].lower() != 'best': cmd += ['--quality', entry['quality']] cmd += ['--range', entry['range']] cmd += ['--vcodec', entry['codec']] if entry['bitrate'] is not None: cmd += ['--vbitrate', str(entry['bitrate'])] if entry['worst']: cmd += ['--worst'] cmd += ['--v-lang',entry["video_language"]] cmd += ['--a-lang', f"{entry['audio_language'] if entry['service'] not in [ 'MMAX'] else 'all'}"] cmd += ['--s-lang', f"{entry['subtitle_language'] if entry['service'] not in [ 'MMAX'] else 'all'}"] if entry['service'] in ['BLBL'] and not entry['audio_channel']: cmd += ['--channels', '2.0'] # else: # cmd += ['--channels', entry['audio_channel']] if entry['start_season'] or entry['start_episode'] or entry['end_season'] or entry['end_episode']: cmd += ['--wanted'] wanted=None if entry['start_season']: wanted = 's'+entry['start_season'] else: wanted = "s01" if entry['start_episode']: if wanted: wanted += ('e'+entry['start_episode']) else: wanted = ('e'+entry['start_episode']) if entry['end_season']: if wanted: wanted += '-s'+entry['end_season'] else: wanted = 's'+entry['end_season'] if entry['end_episode']: if entry['end_season']: if wanted: wanted += ('e'+entry['end_episode']) else: wanted = ('e'+entry['end_episode']) else: if wanted: wanted += ('-s01e'+entry['end_episode']) else: wanted = ('s01e'+entry['end_episode']) cmd += [wanted] # if entry["title_cache"]: # cmd.append('--title-cache') cmd += [entry['service']] if entry['service'] in ['AMZN']: cmd += ["https://www.primevideo.com/detail/"+entry['url']] else: cmd += [entry['url']] # if entry['service'] == 'HS': # cmd += ['--all'] if entry['service'] == 'LT': if entry['title_language']: cmd += ['--title_lang', entry['title_language']] if entry['movie'] and entry['movie'].lower() == 'true': cmd += ['--movie'] if entry['service'] == 'OND': if entry['title_language']: cmd += ['--title_lang', entry['title_language']] if entry['service'] in ['FLX','IT','TVN','BLBL','CR']: if entry['movie'] and entry['movie'].lower() == 'true': cmd += ['--movie'] if entry['service'] == 'IT': if entry['store_front']: cmd += ['--storefront', entry['store_front']] if entry['service'] == 'TID': if entry['season']: cmd += ['--season', str(entry['season'])] # if entry['title_cache']: # cmd += ['--title-cache'] cmd += ['--drm','wv'] if entry['service'] == 'BLBL': if entry['season']: cmd += ['--season', str(entry['season'])] if entry['title_language']: cmd += ['--title_lang', entry['title_language']] if entry['original_url']: cmd += ['--original_url', entry['original_url']] if entry['original_language']: cmd += ['--original_lang', entry['original_language']] # if entry['android'] and entry['android'].lower() == 'true': # cmd += ['--android'] if entry['service'] == 'TVN': if entry['original_language']: cmd += ['--original_lang', entry['original_language']] print(f"Running command: {cmd}") # print(f"Running command:\n{' '.join(cmd)}") embed = discord.Embed( title="🖹 Download Command", description=' '.join(cmd), color=0x0000ff ) await channel.send(embed=embed) result = await asyncio.to_thread(subprocess.run, cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: if '[E]' in result.stdout.decode() or "Processed all titles" not in result.stdout.decode(): embed = discord.Embed( title="❌ Download Failed", description="Download request has been failed.", color=0xff0000 ) embed.add_field(name="🛠 Service", value=entry['service'], inline=True) embed.add_field(name="🔗 URL", value=entry['url'], inline=False) embed.add_field(name="🎥 Quality", value=entry['quality'], inline=True) embed.add_field(name="🎞 Codec", value=entry['codec'], inline=True) embed.add_field(name="🌈 Range", value=entry['range'], inline=True) embed.add_field(name="🎯 Start Season", value=entry['start_season'] or "None", inline=True) embed.add_field(name="🎯 End Season", value=entry['end_season'] or "None", inline=True) embed.add_field(name="📺 Start Episode", value=entry['start_episode'] or "None", inline=True) embed.add_field(name="📺 End Episode", value=entry['end_episode'] or "None", inline=True) embed.add_field(name="🔊 Audio Language", value=entry['audio_language'], inline=False) embed.add_field(name="📜 Subtitle Language", value=entry['subtitle_language'], inline=False) embed.add_field(name="📊 Queue Position", value=len(bot.download_queue), inline=False) embed.add_field(name="📅 Timestamp", value=entry['timestamp'], inline=False) embed.set_footer(text=f"Requested by {entry['user']}") print(result.stdout.decode()) print(f"Error downloading {entry['url']}: ") entry['error'] = result.stdout.decode() entry['status'] = 'failed' await channel.send(embed=embed) else: embed = discord.Embed( title="✅ Download Complete", description="Download request has been completed.", color=0x00ff00 ) embed.add_field(name="🛠 Service", value=entry['service'], inline=True) embed.add_field(name="🔗 URL", value=entry['url'], inline=False) embed.add_field(name="🎥 Quality", value=entry['quality'], inline=True) embed.add_field(name="🎞 Codec", value=entry['codec'], inline=True) embed.add_field(name="🌈 Range", value=entry['range'], inline=True) embed.add_field(name="🎯 Start Season", value=entry['start_season'] or "None", inline=True) embed.add_field(name="🎯 End Season", value=entry['end_season'] or "None", inline=True) embed.add_field(name="📺 Start Episode", value=entry['start_episode'] or "None", inline=True) embed.add_field(name="📺 End Episode", value=entry['end_episode'] or "None", inline=True) embed.add_field(name="🔊 Audio Language", value=entry['audio_language'], inline=False) embed.add_field(name="📜 Subtitle Language", value=entry['subtitle_language'], inline=False) embed.add_field(name="📊 Queue Position", value=len(bot.download_queue), inline=False) embed.add_field(name="📅 Timestamp", value=entry['timestamp'], inline=False) embed.set_footer(text=f"Requested by {entry['user']}") entry['status'] = 'completed' print(f"Download {entry['url']} completed") await channel.send(embed=embed) except Exception as e: print(f"Error processing download {entry['url']}: {e}") embed = discord.Embed( title="❌ Download Error", description="An error occurred while processing the download.", color=0xff0000 ) embed.add_field(name="🛠 Service", value=entry['service'], inline=True) embed.add_field(name="🔗 URL", value=entry['url'], inline=False) embed.add_field(name="🎥 Quality", value=entry['quality'], inline=True) embed.add_field(name="🎞 Codec", value=entry['codec'], inline=True) embed.add_field(name="🌈 Range", value=entry['range'], inline=True) embed.add_field(name="🎯 Start Season", value=entry['start_season'] or "None", inline=True) embed.add_field(name="🎯 End Season", value=entry['end_season'] or "None", inline=True) embed.add_field(name="📺 Start Episode", value=entry['start_episode'] or "None", inline=True) embed.add_field(name="📺 End Episode", value=entry['end_episode'] or "None", inline=True) embed.add_field(name="🔊 Audio Language", value=entry['audio_language'], inline=False) embed.add_field(name="📜 Subtitle Language", value=entry['subtitle_language'], inline=False) embed.add_field(name="📊 Queue Position", value=len(bot.download_queue), inline=False) embed.add_field(name="📅 Timestamp", value=entry['timestamp'], inline=False) embed.set_footer(text=f"Requested by {entry['user']}") await channel.send(embed=embed) bot.save_data() # /check H265 command @bot.tree.command(name="check_codec", description="Check if codec is available") @app_commands.describe( service="Service to use for downloading (e.g., AMZN, NF, HS, VIU, TID, MMAX, BLBL)", url="The URL|ID to check for codec support", codec="Video codec to check (default: H265)", range_="Dynamic range to use (default: SDR)", ) @app_commands.choices(service=[ app_commands.Choice(name="Amazon Prime", value="AMZN"), app_commands.Choice(name="Netflix", value="NF"), app_commands.Choice(name="Hotstar", value="HS"), app_commands.Choice(name="VIU", value="VIU"), app_commands.Choice(name="TrueID", value="TID"), app_commands.Choice(name="Mono Max", value="MMAX"), app_commands.Choice(name="BiliBili", value="BLBL"), ]) @app_commands.choices(codec=[ app_commands.Choice(name="H264", value="H.264"), app_commands.Choice(name="H265", value="H.265"), app_commands.Choice(name="AV1", value="AV1"), app_commands.Choice(name="VP9", value="VP9"), ]) @app_commands.choices(range_=[ app_commands.Choice(name="HDR", value="HDR"), app_commands.Choice(name="SDR", value="SDR"), app_commands.Choice(name="DV", value="DV"), ]) async def check_codec_command( interaction: discord.Interaction, service: str, url: str, codec: str = "H265", range_: Optional[str] = "SDR", ): embed = discord.Embed( title="🛠 H265 Codec Check", description=f"Checking H265 codec availability for URL: {url}", color=0x0000ff ) embed.add_field(name="🛠 Service", value=service, inline=True) embed.add_field(name="🌈 Range", value=range_, inline=True) await interaction.response.send_message(embed=embed) # Check if H265 codec is available for the given URL cmd, codec_available, range_available = check_codec_support(url, codec, service, range_) if codec_available == 'error': embed = discord.Embed( title="❌ Error Checking Codec", description=f"An error occurred while checking codec support for URL: {url}", color=0xff0000 ) await interaction.followup.send(embed=embed) return embed = discord.Embed( title=f"🛠 {codec} Codec Check", description=f"{codec} codec is {'available' if codec_available else 'not available'} for URL: {url}", color=0x00ff00 if codec_available else 0xff0000 if codec_available or codec_available else 0xffa500 ) embed.add_field(name=range_, value='available' if range_available else 'not available', inline=True) embed.add_field(name="Command", value=cmd, inline=False) await interaction.followup.send(embed=embed) # /check H265 command @bot.tree.command(name="clear_temp", description="Clear temporary files") async def clear_temp_command( interaction: discord.Interaction, ): embed = discord.Embed( title="🛠 Clear Temporary Files", description="Clearing temporary files...", color=0x0000ff ) await interaction.response.send_message(embed=embed) # Check if H265 codec is available for the given URL os.removedirs("/root/unshackle-SeFree/Temp") embed = discord.Embed( title="🛠 Temporary Files Cleared", description="Temporary files have been successfully cleared.", color=0x00ff00 ) await interaction.followup.send(embed=embed) def check_codec_support(url: str, codec: str, service: str, range_: str): """Check if H265 codec is available for the given URL""" h264_alias=['h264', 'H264', 'H.264', 'H.264', 'AVC', 'avc', 'AVC1', 'avc1'] h265_alias=['h265', 'H265', 'H.265', 'H.265', 'HEVC', 'hevc', 'HEVC1', 'hevc1'] error_alias=['error', 'Error', 'ERROR', '[E]', '[e]','No tracks returned'] av1_alias=['av1', 'AV1', 'AV1.0', 'av1.0'] vp9_alias=['vp9', 'VP9', 'VP9.0', 'vp9.0'] cmd = ['/root/unshackle-SeFree/.venv/bin/unshackle','dl', '--list', '--wanted','s01e01', '--vcodec', codec, '--range', range_] cmd += [service,url] # Always disable cache for codec checks # if service == 'NF' or service == 'HS': # cmd += ['--all'] try: print(f"Running command: {' '.join(cmd)}") result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if ("Processed all titles" not in result.stdout.decode() or any(alias in result.stdout.decode() for alias in error_alias)): print(f"Error checking codec support for {url}: {result.stdout.decode()}") return ' '.join(cmd),'error','error' # codec check codec_available = False if codec.lower() in h264_alias: if any(alias in result.stdout.decode() for alias in h264_alias): codec_available = True elif codec.lower() in h265_alias: if any(alias in result.stdout.decode() for alias in h265_alias): codec_available = True elif codec.lower() in av1_alias: if any(alias in result.stdout.decode() for alias in av1_alias): codec_available = True elif codec.lower() in vp9_alias: if any(alias in result.stdout.decode() for alias in vp9_alias): codec_available = True if not codec_available: print(f"{codec} codec is not available for {url}") return ' '.join(cmd), codec_available, False print(f"{codec} codec {'is' if codec_available else 'is not'} available for {url}") # Check if HDR is available range_available = False print(f"Checking {range_} support for {url}") if range_ not in result.stdout.decode(): print(f"HDR support not available for {url}") else: print(f"{range_} support available for {url}") range_available = True return ' '.join(cmd), codec_available, range_available except Exception as e: print(f"Exception while checking codec support: {e}") return ' '.join(cmd),'error','error' # /history command @bot.tree.command(name="history", description="List download history") @app_commands.describe( user="Filter by specific user (optional)" ) async def history_command( interaction: discord.Interaction, user: Optional[discord.Member] = None ): embed = discord.Embed(color=0x0099ff) embed.title = "📚 Download History" history = bot.download_history if user: history = [item for item in history if item['user_id'] == user.id] embed.title += f" - {user.display_name}" if not history: embed.description = "No download history found." else: history_list = [] for item in history[-20:]: # Show last 20 status_emoji = "✅" if item['status'] == 'completed' else "❌" if item['status'] == 'failed' else "⏳" if item['status'] == 'in_progress' else "🕔" timestamp = datetime.fromisoformat(item['timestamp']).strftime("%m/%d %H:%M") history_list.append(f"{status_emoji} **{item['id']} **{item['service']} **{item['url']} **{item['quality']} **{item['codec']} **{item['range']}") history_list.append(f" └── {timestamp} • by {item['user']}") embed.description = "\n".join(history_list) if len(history) > 20: embed.set_footer(text=f"Showing last 20 of {len(history)} downloads") await interaction.response.send_message(embed=embed) # Help command @bot.tree.command(name="help", description="Show bot commands and usage") async def help_command(interaction: discord.Interaction): embed = discord.Embed( title="🤖 Bot Commands Help", description="Here are all available commands:", color=0x0099ff ) embed.add_field( name="📥 /download", value="`/download [quality] [codec] [want] [audio_language] [subtitle_language]`\n" "Download a file from the specified URL|ID\n", inline=False ) embed.add_field( name="📋 /history", value="`/history `\n" "List download history for a specific user (or all users if not specified)\n", inline=False ) embed.add_field( name="❓ /help", value="Show this help message", inline=False ) # embed.set_footer(text="Use /keys list to see authorized users and API keys") await interaction.response.send_message(embed=embed) # Error handling # @bot.tree.error # async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError): # channel = bot.get_channel(interaction.channel_id) # if isinstance(error, app_commands.CheckFailure): # embed = discord.Embed( # title="❌ Permission Denied", # description="You don't have permission to use this command.", # color=0xff0000 # ) # if not interaction.response.is_done(): # await interaction.response.send_message(embed=embed, ephemeral=True) # return # embed = discord.Embed( # title="❌ Error", # description=f"An error occurred: {str(error)}", # color=0xff0000 # ) # try: # if interaction.response.is_done(): # await interaction.followup.send(embed=embed, ephemeral=True) # else: # await interaction.response.send_message(embed=embed, ephemeral=True) # except discord.HTTPException: # # If the interaction response is already sent, send a follow-up message # await channel.send(embed=embed) @bot.tree.command(name="my_roles", description="List all roles the bot has in this server") async def my_roles(interaction: discord.Interaction): # Get the bot's Member object in this guild bot_member: discord.Member = interaction.guild.get_member(bot.user.id) if not bot_member: await interaction.response.send_message("Couldn't find myself in this guild.", ephemeral=True) return roles = [role.mention for role in bot_member.roles if role.name != "@everyone"] if roles: await interaction.response.send_message(f"My roles are: {' '.join(roles)}") else: await interaction.response.send_message("I have no roles besides @everyone.") # Run the bot if __name__ == "__main__": token = os.getenv('DISCORD_TOKEN') if not token: print("❌ DISCORD_TOKEN not found in environment variables!") print("Make sure you have a .env file with your bot token.") else: try: bot.run(token) except discord.LoginFailure: print("❌ Invalid bot token! Please check your DISCORD_TOKEN in the .env file.") except Exception as e: print(f"❌ An error occurred: {e}")