From d48de54326d454abfba929e57fe8d33ebb9b442e Mon Sep 17 00:00:00 2001 From: eulaly Date: Sun, 26 Jan 2025 16:57:17 -0500 Subject: [PATCH] initial commit youdis v1 --- .gitignore | Bin 0 -> 73 bytes LICENSE | 24 ++++++++ banner.svg | 31 ++++++++++ dockerfile | 11 ++++ requirements.txt | Bin 0 -> 806 bytes youdis.py | 146 +++++++++++++++++++++++++++++++++++++++++++++++ yt-dlp.conf | 19 ++++++ yt_dlp.png | Bin 0 -> 11815 bytes 8 files changed, 231 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 banner.svg create mode 100644 dockerfile create mode 100644 requirements.txt create mode 100644 youdis.py create mode 100644 yt-dlp.conf create mode 100644 yt_dlp.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..63975df131a282402cdc22353be2a04168752655 GIT binary patch literal 73 zcmWN_F$%yi2tZM1!Bb>&utyTH(zJsqX%Tw+X8MoMa(_<9GUh?pm{1y_>bmAVR5!wk WtQDKy&x)npUX$w?2>}TO1PDLFnG)>) literal 0 HcmV?d00001 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/banner.svg b/banner.svg new file mode 100644 index 0000000..a04107f --- /dev/null +++ b/banner.svg @@ -0,0 +1,31 @@ + + + + + + + + + \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..ecc4a71 --- /dev/null +++ b/dockerfile @@ -0,0 +1,11 @@ +# syntax=docker/dockerfile:1 +FROM python:3.12.1-alpine3.18 +WORKDIR /app +RUN apk update && \ + apk add --no-cache build-base ffmpeg && \ + rm -rf /var/cache/apk/* +COPY requirements.txt requirements.txt +COPY data.json data.json +RUN python3 -m pip install --no-cache-dir -r requirements.txt +COPY youdis.py youdis.py +CMD ["python3", "youdis.py"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..514323317b25bc732fff000d2b4d92ab74e9329d GIT binary patch literal 806 zcmZ9K-A=+#5QO*I#7BY9f)Fp{!q+gyl(t~CrASK@A71@tPyb>_fp%tRXXfg);Jb-$lMmL@CZ_amuN^|v$_JB zS`OZV?K?aM=`{j(<2orW6W@}eY;NYU*2M$%1~`Ab#BJg$*z1COt(If zfN6Wpli>f({f!R)E3_z_T;b47EST5~iE=#E_69?4VxFtbkaq7~$m`MjXxQC%vVs-* zEGz?098r?BC#mehjJ(Q`Tx)Xe+H+eCoR-OhZy1$FcgmmQLK#|oW?U8QuVGs>IsG%p z2YYjpwq05!T24+-D%EhhLe)n|GVSWcx2SbBJ#Adc$Br(zD_w8Lp1#BJJU&s_p-_|K uo&UYB$h!_bU;RxP-&LuHhOY2uq%O(-#Ba`fmvqfG_H0`=h6G9x)cybfn0Ube literal 0 HcmV?d00001 diff --git a/youdis.py b/youdis.py new file mode 100644 index 0000000..099fdfc --- /dev/null +++ b/youdis.py @@ -0,0 +1,146 @@ +#!/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 +import yt_dlp +import json +import asyncio +import threading + +bot = interactions.Client(intents=interactions.Intents.DEFAULT,default_scope=2147491904) +with open('./data.json', 'r') as f: + authorized_users = json.load(f).get('authorized_users') +print(f'authorized_users:{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' + 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) + msg = '' + # 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':'/unraid'}, + 'retries':10, + 'writeinfojson':False, + 'allow_playlist_files':True, + 'noplaylist':True, + 'download_archive':'/unraid/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 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 Ben 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): + if str(user.id) not in authorized_users: + authorized_users.append(str(user.id)) + with open('./data.json','w') as f: #overwrite file - fix later if other params come up + json.dump({'authorized_users':authorized_users}) + print('react:checkmark') + await ctx.message.add_reaction('✅') + +@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): + if str(user.id) in authorized_users: + # ? ? ? fix pls + i = index(authorized_users(str(user.id))) + + # update list, rewrite json + + print('react:checkmark') + await ctx.message.add_reaction('✅') + +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) diff --git a/yt-dlp.conf b/yt-dlp.conf new file mode 100644 index 0000000..c40f24d --- /dev/null +++ b/yt-dlp.conf @@ -0,0 +1,19 @@ +# yt-dlp configuration file +# order matters. yt-dlp always reads this conf +# and runs the first -o that matches the condition + +# Save videos as MP4 +-f bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best + +# Restrict filenames to be Windows-safe +--restrict-filenames + +--match-filter "channel != null" +-o a:/youtube/%(uploader)s/%(title)s.%ext(s) + +# Output template for playlists +--match-filter "playlist_title != null" +-o a:/youtube/%(uploader)s-%(playlist_title)s/%(playlist_index)s-%(title)s.%(ext)s + +# Default output template +-o %(title)s.%(ext)s diff --git a/yt_dlp.png b/yt_dlp.png new file mode 100644 index 0000000000000000000000000000000000000000..68b357f725830d546b846b0349afd417047b7f3d GIT binary patch literal 11815 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 R<#0EZp{|JzO6x`J{{x9`j}`y` literal 0 HcmV?d00001