initial commit youdis v1
This commit is contained in:
BIN
.gitignore
vendored
Normal file
BIN
.gitignore
vendored
Normal file
Binary file not shown.
24
LICENSE
Normal file
24
LICENSE
Normal file
@@ -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 <https://unlicense.org>
|
||||||
31
banner.svg
Normal file
31
banner.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 24 KiB |
11
dockerfile
Normal file
11
dockerfile
Normal file
@@ -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"]
|
||||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
146
youdis.py
Normal file
146
youdis.py
Normal file
@@ -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)
|
||||||
19
yt-dlp.conf
Normal file
19
yt-dlp.conf
Normal file
@@ -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
|
||||||
BIN
yt_dlp.png
Normal file
BIN
yt_dlp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Reference in New Issue
Block a user