840 lines
34 KiB
Python
Executable File
840 lines
34 KiB
Python
Executable File
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('--cdm-only')
|
|
|
|
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 <service> <url> [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 <user>`\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}")
|