Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bin/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
* [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url
* [usb-eject.py](usb-eject.py) adds web-UI buttons to safe-remove usb flashdrives shared through copyparty
* [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder
* [ytdlp-i.py](ytdlp-i.py) is an import-safe hook, based on wget.py, adds yt-dlp and aria2 support
* allows downloading file via URLs, yt-dlp supported links, any link downloadable with aria2 (including magnets)


# general concept demos
Expand Down
148 changes: 148 additions & 0 deletions bin/hooks/ytdlp-i.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env python3

import os
import sys
import json
import subprocess as sp
import threading

from typing import Any


_ = r"""
yt-dlp hook for copyparty. Based on wget.py hook

use copyparty as a file downloader by POSTing URLs as
application/x-www-form-urlencoded (for example using the
📟 message-to-server-log in the web-ui)

this hook is a modified copy of wget.py, modified to
make use of yt-dlp + aria2 in an import-safe way,
so it can be run with the 'I' flag, which speeds up
the startup time of the hook by 140x

example usage as global config:
--xm aw,I,bin/hooks/bin/hooks/ytdlp-i.py

parameters explained,
xm = execute on message-to-server-log
aw = only users with write-access can use this
I = import; do not fork / subprocess

example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xm=aw,I,bin/hooks/ytdlp-i.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^

(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all messages with the params explained above)

example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xm: aw,I,bin/hooks/ytdlp-i.py

the volflag examples only kicks in if you send the message
while you're in the /inc folder (or any folder below there)

Dependencies:
yt-dlp, aria2
Example Dockerfile:
FROM copyparty/ac

ENV PYTHONUNBUFFERED=1
RUN apk add --update --no-cache python3 py3-pip

RUN python3 -m pip config set global.break-system-packages true
RUN python3 -m pip install yt-dlp aria2

RUN yt-dlp --version
RUN aria2c --version

IMPORTANT NOTE:
because this hook uses the 'I' flag to run inside copyparty,
many other flags will not work (f,j,c3,t3600 as seen in the
original wget.py), and furthermore + more importantly we
need to be EXCEPTIONALLY CAREFUL to avoid side-effects, so
the os.chdir has been replaced with cwd=dirpath for example
"""


def helper(ka: dict[str, Any]) -> dict[str, str]:
logger = ka["log"]

url = ka["txt"]
if "://" not in url and url[:6] != "magnet":
url = "https://" + url
proto = url.split("://")[0].lower()
if proto not in ("http", "https", "ftp", "ftps"):
raise Exception(f"ytdlp_hook: Bad protocol {proto}")

path = ka["ap"]

name = url.split("?")[0].split("/")[-1]
tfn = "ytdlp_hook: DOWNLOADING " + name
logger(f"{tfn}\n", 2)
open(tfn, "wb").close()

cmd = [
"yt-dlp",
"-P",
path,
"-f",
"bv*+ba/b",
"--downloader",
"aria2c",
"--downloader-args",
f"-j 16 -x 8 -s 16 -k 1M -d {path}",
url,
]

if url[:6] == "magnet":
cmd = [cmd[4]] + [cmd[7]]

else:
if "." in (name[-3], name[-4]) and name.split(".")[-1] not in (
"php",
"htm",
"aspx",
"html",
):
cmd += ["-o", name]

logger(" ".join(cmd) + "\n", 2)

try:
result = sp.run(cmd, stdout=sp.PIPE, stderr=sp.PIPE, text=True)
result.check_returncode()

t = "ytdlp_hook: DOWNLOAD COMPLETED " + name
logger(f"{t}\n", 2)
stdout, stderr = result.stdout, result.stderr

except sp.CalledProcessError as e:
t = "ytdlp_hook: DOWNLOAD FAILED " + name
logger(f"{t}\n", 4)
open(t, "wb").close()
logger(f"ytdlp_hook: Error: {e}", 4)
stdout, stderr = e.stdout, e.stderr

except sp.SubprocessError as e:
t = "ytdlp_hook: DOWNLOAD FAILED " + name
logger(f"{t}\n", 2)
open(t, "wb").close()
logger(f"ytdlp_hook: Error: {e}", 4)
stdout, stderr = e.stdout, e.stderr

finally:
logger("ytdlp_hook: STDOUT:\n" + stdout, 1)
logger("ytdlp_hook: STDERR:\n" + stderr, 1)
os.unlink(tfn)


def main(inf: dict[str, Any]):
threading.Thread(target=helper, name="ytdlp_hook", args=(inf,), daemon=True).start()