import discord from discord.ext import commands from discord import app_commands from lib.torrent_creator import TorrentUpload from lib.logging_data import logger from lib.sonarr import Sonarr_API from dotenv import load_dotenv from typing import Optional import os import asyncio from pathlib import Path load_dotenv(".env") # Bot configuration intents = discord.Intents.default() intents.message_content = True intents.members = True console = logger(app_name="torrent_uploader",log_dir="./log") 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.upload_queue = [] async def setup_hook(self): """Called when the bot is starting up""" console.log(f"Logged in as {self.user} (ID: {self.user.id})") console.log("------") # Sync slash commands try: synced = await self.tree.sync() console.log(f"Synced {len(synced)} command(s)") # threading.Thread(target=vt_worker).start() # Start the download worker in the background except Exception as e: console.error(f"Failed to sync commands: {e}") bot = DownloadBot() console.client=bot sonarr=Sonarr_API(os.getenv("sonarr_ip"),os.getenv("sonarr_key")) @bot.event async def on_ready(): console.log(f'{bot.user} has connected to Discord!') activity = discord.Game(name="Managing upload | /help") await bot.change_presence(activity=activity) asyncio.create_task(torrent_worker()) @bot.tree.error async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError): 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 ) channel = bot.get_channel(interaction.channel_id) if interaction.response.is_done(): await interaction.followup.send(embed=embed, ephemeral=True) else: await channel.send(embed=embed) console.error(error) # /upload command @bot.tree.command(name="sonarr_upload", description="Create and Upload a torrent file to Torrent server from Sonarr") @app_commands.describe( sonarr_id="Sonarr title id", season="Sonarr season", episode="Sonarr episode", category="Category of the torrent (e.g., Anime, Western Series, Hi-Def Movie)", source_type="Source type of the torrent (e.g., Blu-ray, WEB-DL, Encode)", source="Source of the torrent (e.g., Master, Zoom, TV)", country="Country of the torrent (e.g., Thai, Western, Korean)", original_platform="Original platform of the torrent (e.g., DVD, Hi-def, TV)", is_subdir="Is the torrent in a subdirectory?", bearbit="Bearbit flag (True/False)", torrentdd="TorrentDD flag (True/False)", is_movie="Is this a movie upload? (True/False)", pack="Pack flag (True/False)", # single_season="Single Season (True/False)" ) @app_commands.choices(category=[ ### Anime app_commands.Choice(name="Anime", value="Anime"), ### Series app_commands.Choice(name="Western Series", value="Western Series"), app_commands.Choice(name="Korean Series", value="Korean Series"), app_commands.Choice(name="Japanese Series", value="Japanese Series"), app_commands.Choice(name="Chinese Series", value="Chinese Series"), app_commands.Choice(name="Thai Series", value="Thai Series"), app_commands.Choice(name="Other Series", value="Other Series"), ### Movies app_commands.Choice(name="Hi-Def Movie", value="Hi-Def Movie"), app_commands.Choice(name="4K", value="4K"), ### Documentaries app_commands.Choice(name="Documentary", value="สารคดี"), ]) @app_commands.choices(source_type=[ app_commands.Choice(name="Blu-ray", value="Blu-ray"), app_commands.Choice(name="CD", value="CD"), app_commands.Choice(name="DVD5", value="DVD5"), app_commands.Choice(name="DVD9", value="DVD9"), app_commands.Choice(name="Encode", value="Encode"), app_commands.Choice(name="HD DVD", value="HD DVD"), app_commands.Choice(name="HDTV", value="HDTV"), app_commands.Choice(name="MiniBD", value="MiniBD"), app_commands.Choice(name="Remux", value="Remux"), app_commands.Choice(name="Track", value="Track"), app_commands.Choice(name="WEB-DL", value="WEB-DL"), app_commands.Choice(name="Image", value="Image") ]) @app_commands.choices(source=[ app_commands.Choice(name="Master", value="Master"), app_commands.Choice(name="หนังซูม", value="Zoom"), app_commands.Choice(name="V2D From Master", value="V2D"), app_commands.Choice(name="From TV", value="TV"), app_commands.Choice(name="From HD-TV", value="HD-TV"), app_commands.Choice(name="Hi-def rip from Master", value="Hi-def"), ### Anime app_commands.Choice(name="V2D From Master/DVD Modified", value="From Master/DVD"), app_commands.Choice(name="อัดจาก TV", value="Rip TV"), app_commands.Choice(name="rip from Master", value="Rip Master"), app_commands.Choice(name="Scan", value="scan") ]) @app_commands.choices(country=[ app_commands.Choice(name="Thai", value="Thai"), app_commands.Choice(name="Western", value="Western"), app_commands.Choice(name="Korean", value="Korean"), app_commands.Choice(name="Japanese", value="Japanese"), app_commands.Choice(name="Chinese", value="Chinese"), app_commands.Choice(name="Other", value="Other") ]) @app_commands.choices(original_platform=[ app_commands.Choice(name="DVD", value="DVD"), app_commands.Choice(name="Hi-def", value="Hi-def"), app_commands.Choice(name="TV", value="TV"), app_commands.Choice(name="Books", value="Books"), app_commands.Choice(name="Other", value="Other"), app_commands.Choice(name="Netflix", value="Netflix") ]) @app_commands.choices(is_subdir=[ app_commands.Choice(name="True", value='True'), app_commands.Choice(name="False", value='False') ]) @app_commands.choices(bearbit=[ app_commands.Choice(name="True", value='True'), app_commands.Choice(name="False", value='False') ]) @app_commands.choices(torrentdd=[ app_commands.Choice(name="True", value='True'), app_commands.Choice(name="False", value='False') ]) @app_commands.choices(is_movie=[ app_commands.Choice(name="True", value='True'), app_commands.Choice(name="False", value='False') ]) @app_commands.choices(pack=[ app_commands.Choice(name="True", value='True'), app_commands.Choice(name="False", value='False') ]) # @app_commands.choices(single_season=[ # app_commands.Choice(name="True", value='True'), # app_commands.Choice(name="False", value='False') # ]) async def sonarr_upload_command(interaction: discord.Interaction, sonarr_id: str, season: Optional[str], episode: Optional[str], category: Optional[str], source_type: Optional[str], source: Optional[str], country: Optional[str], original_platform: Optional[str], is_subdir: Optional[str] = "False", bearbit: Optional[str] = "True", torrentdd: Optional[str] = "True", is_movie: Optional[str] = "False", pack: Optional[str] = "False", # single_season: Optional[str] = "False", ): embed = discord.Embed( title="Torrent Upload", description="Creating and uploading torrent file...", color=discord.Color.blue() ) embed.add_field(name="Sonarr title id", value=sonarr_id, inline=False) embed.add_field(name="Season", value=season, inline=False) embed.add_field(name="Episode", value=episode, inline=False) embed.add_field(name="Category", value=category if category else "Not provided", inline=False) embed.add_field(name="Source Type", value=source_type if source_type else "Not provided", inline=False) embed.add_field(name="Source", value=source if source else "Not provided", inline=False) embed.add_field(name="Country", value=country if country else "Not provided", inline=False) embed.add_field(name="Original Platform", value=original_platform if original_platform else "Not provided", inline=False) embed.add_field(name="Is Subdirectory", value=is_subdir if is_subdir else "Not provided", inline=False) embed.add_field(name="BearBit", value=bearbit if bearbit else "Not provided", inline=False) embed.add_field(name="TorrentDD", value=torrentdd if torrentdd else "Not provided", inline=False) embed.add_field(name="Is Movie", value=is_movie if is_movie else "Not provided", inline=False) embed.add_field(name="Pack", value=pack if pack else "Not provided", inline=False) # embed.add_field(name="Single Season", value=pack if pack else "Not provided", inline=False) embed.set_footer(text="This may take a while, please be patient...") await interaction.response.send_message(embed=embed) console.log("Starting torrent creation and upload process...") print_detail=('Sonarr title id:', sonarr_id, '| Sonarr season:', season if season else "Not provided", '| Sonarr episode:', episode if episode else "Not provided", '| category:', category if category else "Not provided", '| source_type:', source_type if source_type else "Not provided", '| source:', source if source else "Not provided", '| country:', country if country else "Not provided", '| original_platform:', original_platform if original_platform else "Not provided", '| is_subdir:', is_subdir, '| bearbit:', bearbit, '| torrentdd:', torrentdd, '| is_movie:', is_movie, '| pack:', pack) console.log("".join(print_detail)) if not sonarr_id: await interaction.followup.send("❌ Sonarr id is required for torrent creation.") return if not category: await interaction.followup.send("❌ Category is required for torrent creation.") return if not source_type: await interaction.followup.send("❌ Source type is required for torrent creation.") return if not source: await interaction.followup.send("❌ Source is required for torrent creation.") return if not country: await interaction.followup.send("❌ Country is required for torrent creation.") return if not original_platform: await interaction.followup.send("❌ Original platform is required for torrent creation.") return if is_subdir not in ['True', 'False']: await interaction.followup.send("❌ is_subdir must be either 'True' or 'False'.") return if bearbit not in ['True', 'False']: await interaction.followup.send("❌ bearbit must be either 'True' or 'False'.") return if torrentdd not in ['True', 'False']: await interaction.followup.send("❌ torrentdd must be either 'True' or 'False'.") return if is_movie not in ['True', 'False']: await interaction.followup.send("❌ is_movie must be either 'True' or 'False'.") return if pack not in ['True', 'False']: await interaction.followup.send("❌ pack must be either 'True' or 'False'.") return is_subdir = is_subdir == 'True' bearbit = bearbit == 'True' torrentdd = torrentdd == 'True' is_movie = is_movie == 'True' pack = pack == 'True' file_path=get_file_path(sonarr,sonarr_id,season=season,episode=episode) entry= { "file_path": file_path["path"], "imdb_id": file_path["imdbId"], "tmdb_id": file_path["tmdbId"], "category": category, "source_type": source_type, "source": source, "country": country, "original_platform": original_platform, "is_subdir": is_subdir, "bearbit": bearbit, "torrentdd": torrentdd, "is_movie": is_movie, 'channel_id': interaction.channel_id, 'pack': pack, # 'single_season' : single_season } # Add the entry to the upload queue bot.upload_queue.append(entry) console.log(f"Added to upload queue: {entry}") # Start the torrent worker if not already running embed = discord.Embed( title="Torrent Upload", description="Torrent creation and upload process has been started. You will be notified once the process is completed.", color=discord.Color.green() ) embed.set_footer(text="Please wait while the bot processes your request...") await interaction.followup.send(embed=embed) def get_file_path(sonarr:Sonarr_API,sonarr_id,season,episode): sonarr_series=sonarr.get_series_detail(sonarr_id) tmdbId=sonarr_series["tmdbId"] imdbId=sonarr_series["imdbId"] sonarr_seasons=sonarr_series["seasons"] if season is not None and episode is None: for ss in sonarr_seasons: if int(season)==int(ss["seasonNumber"]): if season>0: file_path=Path.joinpath(Path(sonarr_series["path"]),f"S{season:02}").__str__() return {"path":file_path,"tmdbId":tmdbId,"imdbId":imdbId} else: file_path=Path.joinpath(Path(sonarr_series["path"]),"Specials").__str__() return {"path":file_path,"tmdbId":tmdbId,"imdbId":imdbId} elif season is not None and episode is not None: season_episode_list=sonarr.get_episode_detail_from_season(sonarr_id,season=season) for ep in season_episode_list: if int(ep["seasonNumber"] )==int(season) and int(ep["episodeNumber"] )==int(episode) : ep_detail=sonarr.get_episode_detail(ep["id"]) file_path=ep_detail["episodeFile"]["path"] return {"path":file_path,"tmdbId":tmdbId,"imdbId":imdbId} else: file_path=sonarr_series["path"] return {"path":file_path,"tmdbId":tmdbId,"imdbId":imdbId} async def torrent_worker(): """Continuously process the upload queue in the background""" while True: if bot.upload_queue: discord_webhook=os.getenv('torrent_update_webhook').split(",") entry = bot.upload_queue.pop(0) channel=bot.get_channel(entry['channel_id']) torrentupload=TorrentUpload(console=console) status = await torrentupload.upload_torrent(entry) if not (any(item['status'] is False for item in status['bearbit']) or any(item['status'] is False for item in status['torrentdd'])): embed = discord.Embed( title="Torrent Upload Completed", description="All torrents have been created and uploaded successfully.", color=discord.Color.green() ) bb_text="" for staBB in status['bearbit']: bb_text+=f"{"Uploaded" if staBB["status"] else "Fail Upload"} : [{staBB["name"]}]({staBB["url"]})" bb_text+="\n" # bb_text+=staBB["url"] # bb_text+="\n" embed.add_field(name="BearBit", value=bb_text, inline=False) dd_text="" for staDD in status['torrentdd']: dd_text+=f"{"Uploaded" if staDD["status"] else "Fail Upload"} : [{staDD["name"]}]({staDD["url"]})" dd_text+="\n" # dd_text+=staBB["url"] # dd_text+="\n" embed.add_field(name="TorrentDD", value=dd_text, inline=False) # if status['bearbit'] or status['torrentdd']: # for i,torrent in enumerate(torrent_files['all']): # embed.add_field(name="Title", value=os.path.basename(torrent).replace(".all.torrent",""), inline=False) # embed.add_field(name="BearBit", value="[Click here to visit BearBit](https://bearbit.org/)", inline=False) # embed.add_field(name="TorrentDD", value="[Click here to visit TorrentDD](https://torrentdd.com/)", inline=False) embed.set_footer(text="Thank you for visiting!") console.debug(status) console.log('Torrent Upload Completed',is_discord={"channel": channel,"embed": embed,"web_hook_urls":discord_webhook}) else: embed = discord.Embed( title="Torrent Upload Failed", description="Some or all torrents were fail to uploaded. Please check the logs for details.", color=discord.Color.red() if (any(item['status'] is False for item in status['bearbit']) and any(item['status'] is False for item in status['torrentdd'])) else discord.Color.orange() ) bb_text="" # torrent_name=None for staBB in status['bearbit']: bb_text+=f"{"Uploaded" if staBB["status"] else "Fail Upload"} : {staBB["name"]}" bb_text+="\n" embed.add_field(name="BearBit", value=bb_text, inline=False) dd_text="" for staDD in status['torrentdd']: dd_text+=f"{"Uploaded" if staDD["status"] else "Fail Upload"} : {staDD["name"]}" dd_text+="\n" embed.add_field(name="TorrentDD", value=dd_text, inline=False) embed.set_footer(text="Please check the logs for more details.") console.debug(status) console.error('Torrent Upload Failed',is_discord={"channel": channel,"embed": embed}) else: await asyncio.sleep(5) # Sleep briefly if queue is empty if __name__ == "__main__": token = os.getenv('TORRENT_DISCORD_TOKEN') if not token: console.error("❌ TORRENT_DISCORD_TOKEN not found in environment variables!") console.error("Make sure you have a .env file with your bot token.") else: try: bot.run(token) except discord.LoginFailure: console.error("❌ Invalid bot token! Please check your TORRENT_DISCORD_TOKEN in the .env file.") except Exception as e: console.error(f"❌ An error occurred: {e}")