#!/usr/bin/env python3 ''' youdis v1.1 bot for downloading youtube videos using yt-dlp discord-py-interactions 5.11.0 has new option requires python>=3.9 ''' # match_filter: info_dict -> Raise utils.DownloadCancelled(msg) ? interrupt import interactions from os import getenv from pathlib import Path import yt_dlp import json import asyncio import threading userFile = Path('/config/users.json') userFile.parent.mkdir(exist_ok=True, parents=True) bot = interactions.Client(intents=interactions.Intents.DEFAULT,default_scope=2147491904) def save_authorized_users(authorized_users): with open(userFile, 'w') as f: json.dump({'authorized_users': authorized_users}, f) def load_authorized_users(): if not userFile.exists(): save_authorized_users([]) print(f'users.json not found; saving to {userFile}') return [] try: with open(userFile, 'r') as f: data = json.load(f) except (json.JSONDecodeError, OSError): save_authorized_users([]) print(f'users.json invalid; resetting {userFile}') return [] authorized_users = data.get('authorized_users', []) if not isinstance(authorized_users, list): authorized_users = [] authorized_users = [str(user_id) for user_id in authorized_users] save_authorized_users(authorized_users) print(f'authorized_users:{authorized_users}') return authorized_users authorized_users = load_authorized_users() title = '' async def send_message(ctx, message): await ctx.author.send(message) def download_video(url, options): with yt_dlp.YoutubeDL(options) as ydl: ydl.download(url) def create_hook(ctx,loop): def hook(d): global title status = d.get('status') if status == 'error': msg = f'error; video probably already exists, have you checked archive.txt' asyncio.run_coroutine_threadsafe(send_message(ctx,msg),loop) elif d.get('info_dict').get('title') != title: title = d.get('info_dict').get('title') playlist_index = d.get('info_dict').get('playlist_index') playlist_count = d.get('info_dict').get('playlist_count') filename = d.get('filename') url = d.get('info_dict').get('webpage_url') msg = f'{status} {playlist_index} of {playlist_count}: {filename} <{url}>' asyncio.run_coroutine_threadsafe(send_message(ctx,msg),loop) return hook @interactions.slash_command(name="youtube",description="download video from youtube to server") @interactions.slash_option( name='url', opt_type=interactions.OptionType.STRING, required=True, description='url target' ) async def youtube(ctx: interactions.SlashContext, url:str): print(f'{ctx.author.id} requested {url}') loop = asyncio.get_running_loop() hook = create_hook(ctx,loop) # use api_to_cli and paste cli options to get the output you need yoptions = { 'format':'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', 'fragment_tries': 10, 'restrictfilenames':True, 'paths': {'home':'/downloads'}, 'retries':10, 'writeinfojson':False, 'allow_playlist_files':True, 'noplaylist':True, 'download_archive':'/config/archive.txt', 'progress_hooks':[hook], 'outtmpl': '%(uploader)s/%(playlist_title)s/%(playlist_index)s%(playlist_index& - )s%(title)s.%(ext)s', 'outtmpl_na_placeholder':'', } # check that user is authorized if str(ctx.author.id) not in authorized_users: if ctx.author.id == 127831327012683776: await ctx.author.send('potato stop') await ctx.author.send('you are not authorized to use this command. message my owner to be added.') return else: await ctx.channel.send(f'Downloading from <{url}>. Status updates via DM.') #await ctx.defer() #if you need up to 15m to respond # 1/2 - download in separate thread, else progress_hook blocks downstream async ctx.send download_thread = threading.Thread(target=download_video, args=(url,yoptions)) download_thread.start() await asyncio.to_thread(download_thread.join) # 2/2 - replace the above with this next try: #try: # await asyncio.to_thread(download_video, url, yoptions) #except Exception as e: # print(f"download failed: {e}") # await ctx.author.send(f"download failed: {str(e)}") @interactions.slash_command(name="interrupt",description="cancel current job") @interactions.check(interactions.is_owner()) async def _interrupt(ctx): # interrupt here print('interrupting current job - not implemented') await ctx.author.send('interrupting current job - not implemented') @interactions.slash_command(name="adduser",description="authorize target user") @interactions.slash_option( name="user", opt_type=interactions.OptionType.USER, required=True, description='enable this bot for target user', ) @interactions.check(interactions.is_owner()) async def _adduser(ctx: interactions.SlashContext, user:interactions.OptionType.USER): user_id = str(user.id) if user_id not in authorized_users: authorized_users.append(user_id) save_authorized_users(authorized_users) print(f'authorized {user_id}') await ctx.author.send(f'authorized {user.mention}') else: await ctx.author.send(f'{user.mention} is already authorized') @interactions.slash_command(name="removeuser",description="deauthorize target user") @interactions.slash_option( name="user", opt_type=interactions.OptionType.USER, required=True, description='disable this bot for target user', ) @interactions.check(interactions.is_owner()) async def _removeuser(ctx: interactions.SlashContext, user:interactions.OptionType.USER): user_id = str(user.id) if user_id in authorized_users: authorized_users.remove(user_id) save_authorized_users(authorized_users) print(f'deauthorized {user_id}') await ctx.author.send(f'deauthorized {user.mention}') else: await ctx.author.send(f'{user.mention} is not currently authorized') async def dl_hook(d): msg = f'{d["status"]} {d["filename"]}' print(msg) await ctx.author.send(msg) api_token = getenv('api_token') if not api_token: raise ValueError('API token not set. Retrieve from your Discord bot.') bot.start(api_token)