commit d48de54326d454abfba929e57fe8d33ebb9b442e Author: eulaly Date: Sun Jan 26 16:57:17 2025 -0500 initial commit youdis v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..63975df Binary files /dev/null and b/.gitignore differ 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 0000000..5143233 Binary files /dev/null and b/requirements.txt differ 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 0000000..68b357f Binary files /dev/null and b/yt_dlp.png differ