ytp-dl 0.6.9__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scripts/api.py +186 -2
- scripts/downloader.py +195 -9
- {ytp_dl-0.6.9.dist-info → ytp_dl-0.7.0.dist-info}/METADATA +5 -5
- ytp_dl-0.7.0.dist-info/RECORD +8 -0
- ytp_dl-0.6.9.dist-info/RECORD +0 -8
- {ytp_dl-0.6.9.dist-info → ytp_dl-0.7.0.dist-info}/WHEEL +0 -0
- {ytp_dl-0.6.9.dist-info → ytp_dl-0.7.0.dist-info}/entry_points.txt +0 -0
- {ytp_dl-0.6.9.dist-info → ytp_dl-0.7.0.dist-info}/top_level.txt +0 -0
scripts/api.py
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
6
|
+
import re
|
|
5
7
|
import shutil
|
|
6
8
|
import tempfile
|
|
7
9
|
import time
|
|
8
10
|
from threading import BoundedSemaphore, Lock
|
|
9
11
|
|
|
10
|
-
from flask import Flask, request, send_file,
|
|
12
|
+
from flask import Flask, Response, jsonify, request, send_file, stream_with_context
|
|
11
13
|
|
|
12
|
-
from .downloader import
|
|
14
|
+
from .downloader import (
|
|
15
|
+
download_video,
|
|
16
|
+
download_video_stream,
|
|
17
|
+
validate_environment,
|
|
18
|
+
)
|
|
13
19
|
|
|
14
20
|
app = Flask(__name__)
|
|
15
21
|
|
|
@@ -30,6 +36,8 @@ STALE_JOB_TTL_S = int(os.environ.get("YTPDL_STALE_JOB_TTL_S", "3600"))
|
|
|
30
36
|
|
|
31
37
|
_ALLOWED_EXTENSIONS = {"mp3", "mp4", "best"}
|
|
32
38
|
|
|
39
|
+
_JOB_ID_RX = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
|
|
40
|
+
|
|
33
41
|
|
|
34
42
|
def _cleanup_stale_jobs() -> None:
|
|
35
43
|
now = time.time()
|
|
@@ -48,6 +56,14 @@ def _cleanup_stale_jobs() -> None:
|
|
|
48
56
|
pass
|
|
49
57
|
|
|
50
58
|
|
|
59
|
+
def _safe_job_id(raw: str | None) -> str:
|
|
60
|
+
s = (raw or "").strip()
|
|
61
|
+
if _JOB_ID_RX.match(s):
|
|
62
|
+
return s
|
|
63
|
+
# fallback to timestamp-based id (stable & filesystem-safe)
|
|
64
|
+
return str(int(time.time() * 1000))
|
|
65
|
+
|
|
66
|
+
|
|
51
67
|
def _try_acquire_job_slot() -> bool:
|
|
52
68
|
global _in_use
|
|
53
69
|
if not _sem.acquire(blocking=False):
|
|
@@ -67,6 +83,11 @@ def _release_job_slot() -> None:
|
|
|
67
83
|
|
|
68
84
|
@app.route("/api/download", methods=["POST"])
|
|
69
85
|
def handle_download():
|
|
86
|
+
"""
|
|
87
|
+
Backwards-compatible direct download endpoint:
|
|
88
|
+
- runs yt-dlp
|
|
89
|
+
- returns the file body directly
|
|
90
|
+
"""
|
|
70
91
|
_cleanup_stale_jobs()
|
|
71
92
|
|
|
72
93
|
if not _try_acquire_job_slot():
|
|
@@ -145,6 +166,169 @@ def handle_download():
|
|
|
145
166
|
return jsonify(error=f"Download failed: {str(e)}"), 500
|
|
146
167
|
|
|
147
168
|
|
|
169
|
+
@app.route("/api/download_sse", methods=["POST"])
|
|
170
|
+
def handle_download_sse():
|
|
171
|
+
"""
|
|
172
|
+
New SSE endpoint:
|
|
173
|
+
- streams raw yt-dlp stdout lines in real-time (SSE)
|
|
174
|
+
- stores result.json with the final file path in a stable job dir
|
|
175
|
+
- releases concurrency slot immediately when yt-dlp completes
|
|
176
|
+
- client fetches the final file via /api/fetch/<job_id>
|
|
177
|
+
"""
|
|
178
|
+
_cleanup_stale_jobs()
|
|
179
|
+
|
|
180
|
+
if not _try_acquire_job_slot():
|
|
181
|
+
return jsonify(error="Server busy, try again later"), 503
|
|
182
|
+
|
|
183
|
+
released = False
|
|
184
|
+
|
|
185
|
+
def _release_once() -> None:
|
|
186
|
+
nonlocal released
|
|
187
|
+
if not released:
|
|
188
|
+
released = True
|
|
189
|
+
_release_job_slot()
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
data = request.get_json(force=True)
|
|
193
|
+
url = (data.get("url") or "").strip()
|
|
194
|
+
resolution = data.get("resolution")
|
|
195
|
+
extension = (data.get("extension") or "mp4").strip().lower()
|
|
196
|
+
|
|
197
|
+
if not url:
|
|
198
|
+
_release_once()
|
|
199
|
+
return jsonify(error="Missing 'url'"), 400
|
|
200
|
+
|
|
201
|
+
if extension not in _ALLOWED_EXTENSIONS:
|
|
202
|
+
_release_once()
|
|
203
|
+
return jsonify(
|
|
204
|
+
error=f"Invalid 'extension'. Allowed: {sorted(_ALLOWED_EXTENSIONS)}"
|
|
205
|
+
), 400
|
|
206
|
+
|
|
207
|
+
job_id = _safe_job_id(data.get("job_id"))
|
|
208
|
+
job_dir = os.path.join(BASE_DOWNLOAD_DIR, job_id)
|
|
209
|
+
shutil.rmtree(job_dir, ignore_errors=True)
|
|
210
|
+
os.makedirs(job_dir, exist_ok=True)
|
|
211
|
+
|
|
212
|
+
result_path = os.path.join(job_dir, "result.json")
|
|
213
|
+
err_path = os.path.join(job_dir, "error.txt")
|
|
214
|
+
|
|
215
|
+
def gen():
|
|
216
|
+
file_path: str | None = None
|
|
217
|
+
try:
|
|
218
|
+
# optional "ready" marker
|
|
219
|
+
yield f"data: [vps_ready] {job_id}\n\n"
|
|
220
|
+
|
|
221
|
+
dl = download_video_stream(
|
|
222
|
+
url=url,
|
|
223
|
+
resolution=resolution,
|
|
224
|
+
extension=extension,
|
|
225
|
+
out_dir=job_dir,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# manually iterate so we can capture StopIteration.value (the final path)
|
|
229
|
+
while True:
|
|
230
|
+
try:
|
|
231
|
+
line = next(dl)
|
|
232
|
+
except StopIteration as si:
|
|
233
|
+
file_path = si.value
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
if line:
|
|
237
|
+
yield f"data: {line}\n\n"
|
|
238
|
+
|
|
239
|
+
if not (file_path and os.path.exists(file_path)):
|
|
240
|
+
raise RuntimeError("Download completed but output file not found")
|
|
241
|
+
|
|
242
|
+
# Persist result for /api/fetch/<job_id>
|
|
243
|
+
try:
|
|
244
|
+
with open(result_path, "w", encoding="utf-8") as f:
|
|
245
|
+
json.dump(
|
|
246
|
+
{
|
|
247
|
+
"job_id": job_id,
|
|
248
|
+
"file_path": os.path.abspath(file_path),
|
|
249
|
+
"basename": os.path.basename(file_path),
|
|
250
|
+
"created_ts": time.time(),
|
|
251
|
+
},
|
|
252
|
+
f,
|
|
253
|
+
ensure_ascii=False,
|
|
254
|
+
)
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
# Release slot as soon as yt-dlp is done (do NOT hold slot for fetch transfer)
|
|
259
|
+
_release_once()
|
|
260
|
+
|
|
261
|
+
yield f"data: [vps_done] {os.path.basename(file_path)}\n\n"
|
|
262
|
+
yield "data: [vps_end]\n\n"
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
try:
|
|
267
|
+
with open(err_path, "w", encoding="utf-8") as f:
|
|
268
|
+
f.write(str(e))
|
|
269
|
+
except Exception:
|
|
270
|
+
pass
|
|
271
|
+
|
|
272
|
+
_release_once()
|
|
273
|
+
msg = str(e)
|
|
274
|
+
# keep it as a single SSE line
|
|
275
|
+
yield f"data: [vps_error] {msg}\n\n"
|
|
276
|
+
yield "data: [vps_end]\n\n"
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
finally:
|
|
280
|
+
# ensure slot is released even if client disconnects mid-stream
|
|
281
|
+
_release_once()
|
|
282
|
+
|
|
283
|
+
resp = Response(stream_with_context(gen()), content_type="text/event-stream")
|
|
284
|
+
resp.headers["Cache-Control"] = "no-cache"
|
|
285
|
+
resp.headers["X-Accel-Buffering"] = "no"
|
|
286
|
+
return resp
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
_release_once()
|
|
290
|
+
return jsonify(error=f"Download failed: {str(e)}"), 500
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@app.route("/api/fetch/<job_id>", methods=["GET"])
|
|
294
|
+
def fetch_job_file(job_id: str):
|
|
295
|
+
"""
|
|
296
|
+
Fetch the finished file for a job_id created by /api/download_sse.
|
|
297
|
+
Cleans up the job dir after response is fully consumed.
|
|
298
|
+
"""
|
|
299
|
+
job_id = _safe_job_id(job_id)
|
|
300
|
+
job_dir = os.path.join(BASE_DOWNLOAD_DIR, job_id)
|
|
301
|
+
result_path = os.path.join(job_dir, "result.json")
|
|
302
|
+
|
|
303
|
+
if not os.path.exists(result_path):
|
|
304
|
+
return jsonify(error="Job not found or not complete"), 404
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
with open(result_path, "r", encoding="utf-8") as f:
|
|
308
|
+
meta = json.load(f)
|
|
309
|
+
file_path = meta.get("file_path") or ""
|
|
310
|
+
except Exception:
|
|
311
|
+
return jsonify(error="Job metadata unreadable"), 500
|
|
312
|
+
|
|
313
|
+
file_path = os.path.abspath(file_path)
|
|
314
|
+
if not file_path.startswith(os.path.abspath(job_dir) + os.sep):
|
|
315
|
+
return jsonify(error="Invalid job file path"), 500
|
|
316
|
+
|
|
317
|
+
if not os.path.exists(file_path):
|
|
318
|
+
return jsonify(error="Job file missing"), 404
|
|
319
|
+
|
|
320
|
+
response = send_file(file_path, as_attachment=True)
|
|
321
|
+
|
|
322
|
+
def _cleanup() -> None:
|
|
323
|
+
try:
|
|
324
|
+
shutil.rmtree(job_dir, ignore_errors=True)
|
|
325
|
+
except Exception:
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
response.call_on_close(_cleanup)
|
|
329
|
+
return response
|
|
330
|
+
|
|
331
|
+
|
|
148
332
|
@app.route("/healthz", methods=["GET"])
|
|
149
333
|
def healthz():
|
|
150
334
|
with _in_use_lock:
|
scripts/downloader.py
CHANGED
|
@@ -6,7 +6,8 @@ import shlex
|
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
8
|
import time
|
|
9
|
-
from
|
|
9
|
+
from collections import deque
|
|
10
|
+
from typing import Deque, Generator, List, Optional, Tuple
|
|
10
11
|
|
|
11
12
|
# =========================
|
|
12
13
|
# Config / constants
|
|
@@ -60,6 +61,13 @@ def _tail(out: str) -> str:
|
|
|
60
61
|
return txt.strip()
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
def _tail_deque(lines: Deque[str]) -> str:
|
|
65
|
+
txt = "\n".join(list(lines)[-_MAX_ERR_LINES:])
|
|
66
|
+
if len(txt) > _MAX_ERR_CHARS:
|
|
67
|
+
txt = txt[-_MAX_ERR_CHARS:]
|
|
68
|
+
return txt.strip()
|
|
69
|
+
|
|
70
|
+
|
|
63
71
|
def _is_youtube_url(url: str) -> bool:
|
|
64
72
|
u = (url or "").lower()
|
|
65
73
|
return any(h in u for h in ("youtube.com", "youtu.be", "youtube-nocookie.com"))
|
|
@@ -129,14 +137,19 @@ def _common_flags() -> List[str]:
|
|
|
129
137
|
# --no-playlist prevents accidental channel/playlist pulls (and disk blowups)
|
|
130
138
|
return [
|
|
131
139
|
"--no-playlist",
|
|
132
|
-
"--retries",
|
|
133
|
-
"
|
|
134
|
-
"--
|
|
135
|
-
"
|
|
140
|
+
"--retries",
|
|
141
|
+
"10",
|
|
142
|
+
"--fragment-retries",
|
|
143
|
+
"10",
|
|
144
|
+
"--retry-sleep",
|
|
145
|
+
"exp=1:30",
|
|
146
|
+
"--user-agent",
|
|
147
|
+
MODERN_UA,
|
|
136
148
|
"--no-cache-dir",
|
|
137
149
|
"--ignore-config",
|
|
138
150
|
"--embed-metadata",
|
|
139
|
-
"--sleep-interval",
|
|
151
|
+
"--sleep-interval",
|
|
152
|
+
"1",
|
|
140
153
|
]
|
|
141
154
|
|
|
142
155
|
|
|
@@ -249,11 +262,14 @@ def _download_with_format(
|
|
|
249
262
|
|
|
250
263
|
argv = [
|
|
251
264
|
YTDLP_BIN,
|
|
252
|
-
"-f",
|
|
265
|
+
"-f",
|
|
266
|
+
fmt,
|
|
253
267
|
*(_common_flags()),
|
|
254
|
-
"--output",
|
|
268
|
+
"--output",
|
|
269
|
+
out_tpl,
|
|
255
270
|
# Ensure we can reliably pick the final output path.
|
|
256
|
-
"--print",
|
|
271
|
+
"--print",
|
|
272
|
+
"after_move:filepath",
|
|
257
273
|
]
|
|
258
274
|
|
|
259
275
|
if extract_mp3:
|
|
@@ -292,6 +308,108 @@ def _fmt_best(cap: int) -> str:
|
|
|
292
308
|
return f"bv*[height<={cap}]+ba/b[height<={cap}]"
|
|
293
309
|
|
|
294
310
|
|
|
311
|
+
def _download_with_format_stream(
|
|
312
|
+
url: str,
|
|
313
|
+
out_dir: str,
|
|
314
|
+
fmt: str,
|
|
315
|
+
merge_output_format: Optional[str] = None,
|
|
316
|
+
extract_mp3: bool = False,
|
|
317
|
+
) -> Generator[str, None, str]:
|
|
318
|
+
"""
|
|
319
|
+
Stream yt-dlp stdout lines in real-time.
|
|
320
|
+
Yields: raw stdout lines
|
|
321
|
+
Returns (StopIteration.value): absolute final output path
|
|
322
|
+
"""
|
|
323
|
+
out_dir = os.path.abspath(out_dir)
|
|
324
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
325
|
+
out_tpl = os.path.join(out_dir, "%(title)s.%(ext)s")
|
|
326
|
+
|
|
327
|
+
argv: List[str] = [
|
|
328
|
+
YTDLP_BIN,
|
|
329
|
+
"-f",
|
|
330
|
+
fmt,
|
|
331
|
+
*(_common_flags()),
|
|
332
|
+
# ensure progress is emitted as real lines (no carriage-return overwrites)
|
|
333
|
+
"--progress",
|
|
334
|
+
"--newline",
|
|
335
|
+
"--output",
|
|
336
|
+
out_tpl,
|
|
337
|
+
"--print",
|
|
338
|
+
"after_move:filepath",
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
if extract_mp3:
|
|
342
|
+
argv.extend(["--extract-audio", "--audio-format", "mp3"])
|
|
343
|
+
|
|
344
|
+
if merge_output_format:
|
|
345
|
+
argv.extend(["--merge-output-format", merge_output_format])
|
|
346
|
+
|
|
347
|
+
argv.append(url)
|
|
348
|
+
|
|
349
|
+
tail_lines: Deque[str] = deque(maxlen=_MAX_ERR_LINES)
|
|
350
|
+
# Keep only a bounded capture for path extraction.
|
|
351
|
+
cap_lines: Deque[str] = deque(maxlen=2000)
|
|
352
|
+
|
|
353
|
+
proc = subprocess.Popen(
|
|
354
|
+
argv,
|
|
355
|
+
stdout=subprocess.PIPE,
|
|
356
|
+
stderr=subprocess.STDOUT,
|
|
357
|
+
text=True,
|
|
358
|
+
bufsize=1,
|
|
359
|
+
universal_newlines=True,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
assert proc.stdout is not None
|
|
364
|
+
for raw in iter(proc.stdout.readline, ""):
|
|
365
|
+
line = (raw or "").rstrip("\n")
|
|
366
|
+
if not line:
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
tail_lines.append(line)
|
|
370
|
+
cap_lines.append(line)
|
|
371
|
+
yield line
|
|
372
|
+
|
|
373
|
+
proc.stdout.close()
|
|
374
|
+
rc = proc.wait()
|
|
375
|
+
|
|
376
|
+
out_joined = "\n".join(cap_lines)
|
|
377
|
+
path = _extract_final_path(out_joined, out_dir)
|
|
378
|
+
|
|
379
|
+
if path and os.path.exists(path):
|
|
380
|
+
return os.path.abspath(path)
|
|
381
|
+
|
|
382
|
+
# fallback: newest non-temp file in out_dir
|
|
383
|
+
try:
|
|
384
|
+
best_path = None
|
|
385
|
+
best_mtime = -1.0
|
|
386
|
+
for name in os.listdir(out_dir):
|
|
387
|
+
if name.endswith((".part", ".ytdl", ".tmp")):
|
|
388
|
+
continue
|
|
389
|
+
full = os.path.join(out_dir, name)
|
|
390
|
+
if not os.path.isfile(full):
|
|
391
|
+
continue
|
|
392
|
+
mt = os.path.getmtime(full)
|
|
393
|
+
if mt > best_mtime:
|
|
394
|
+
best_mtime = mt
|
|
395
|
+
best_path = full
|
|
396
|
+
if best_path and os.path.exists(best_path):
|
|
397
|
+
return os.path.abspath(best_path)
|
|
398
|
+
except Exception:
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
if rc != 0:
|
|
402
|
+
raise RuntimeError(f"yt-dlp failed (format: {fmt})\n{_tail_deque(tail_lines)}")
|
|
403
|
+
raise RuntimeError(f"Download completed but output file not found (format: {fmt})\n{_tail_deque(tail_lines)}")
|
|
404
|
+
|
|
405
|
+
finally:
|
|
406
|
+
try:
|
|
407
|
+
if proc.poll() is None:
|
|
408
|
+
proc.kill()
|
|
409
|
+
except Exception:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
|
|
295
413
|
# =========================
|
|
296
414
|
# Public API
|
|
297
415
|
# =========================
|
|
@@ -361,3 +479,71 @@ def download_video(
|
|
|
361
479
|
finally:
|
|
362
480
|
if _mullvad_present():
|
|
363
481
|
_run_argv(["mullvad", "disconnect"], check=False)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def download_video_stream(
|
|
485
|
+
url: str,
|
|
486
|
+
resolution: int | None = 1080,
|
|
487
|
+
extension: Optional[str] = None,
|
|
488
|
+
out_dir: str = DEFAULT_OUT_DIR,
|
|
489
|
+
) -> Generator[str, None, str]:
|
|
490
|
+
"""
|
|
491
|
+
Streaming variant for SSE usage.
|
|
492
|
+
Yields raw yt-dlp stdout lines; returns final file path as StopIteration.value.
|
|
493
|
+
"""
|
|
494
|
+
if not url:
|
|
495
|
+
raise RuntimeError("Missing URL")
|
|
496
|
+
|
|
497
|
+
out_dir = os.path.abspath(out_dir)
|
|
498
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
499
|
+
|
|
500
|
+
validate_environment()
|
|
501
|
+
|
|
502
|
+
require_mullvad_login()
|
|
503
|
+
mullvad_connect(MULLVAD_LOCATION)
|
|
504
|
+
if not mullvad_wait_connected():
|
|
505
|
+
raise RuntimeError("Mullvad connection failed")
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
mode = (extension or "mp4").lower().strip()
|
|
509
|
+
|
|
510
|
+
if mode == "mp3":
|
|
511
|
+
return (yield from _download_with_format_stream(
|
|
512
|
+
url=url,
|
|
513
|
+
out_dir=out_dir,
|
|
514
|
+
fmt="bestaudio",
|
|
515
|
+
merge_output_format=None,
|
|
516
|
+
extract_mp3=True,
|
|
517
|
+
))
|
|
518
|
+
|
|
519
|
+
cap = int(resolution or 1080)
|
|
520
|
+
|
|
521
|
+
if mode == "best":
|
|
522
|
+
try:
|
|
523
|
+
return (yield from _download_with_format_stream(
|
|
524
|
+
url=url,
|
|
525
|
+
out_dir=out_dir,
|
|
526
|
+
fmt=_fmt_best(cap),
|
|
527
|
+
merge_output_format=None,
|
|
528
|
+
extract_mp3=False,
|
|
529
|
+
))
|
|
530
|
+
except Exception:
|
|
531
|
+
return (yield from _download_with_format_stream(
|
|
532
|
+
url=url,
|
|
533
|
+
out_dir=out_dir,
|
|
534
|
+
fmt=_fmt_mp4_apple_safe(cap),
|
|
535
|
+
merge_output_format="mp4",
|
|
536
|
+
extract_mp3=False,
|
|
537
|
+
))
|
|
538
|
+
|
|
539
|
+
return (yield from _download_with_format_stream(
|
|
540
|
+
url=url,
|
|
541
|
+
out_dir=out_dir,
|
|
542
|
+
fmt=_fmt_mp4_apple_safe(cap),
|
|
543
|
+
merge_output_format="mp4",
|
|
544
|
+
extract_mp3=False,
|
|
545
|
+
))
|
|
546
|
+
|
|
547
|
+
finally:
|
|
548
|
+
if _mullvad_present():
|
|
549
|
+
_run_argv(["mullvad", "disconnect"], check=False)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ytp-dl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: YouTube video downloader with Mullvad VPN integration and Flask API
|
|
5
5
|
Home-page: https://github.com/yourusername/ytp-dl
|
|
6
6
|
Author: dumgum82
|
|
@@ -57,7 +57,7 @@ A lightweight YouTube downloader with Mullvad VPN integration and an HTTP API.
|
|
|
57
57
|
## Installation
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
pip install ytp-dl==0.
|
|
60
|
+
pip install ytp-dl==0.7.0 yt-dlp[default]
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
Requirements:
|
|
@@ -242,7 +242,7 @@ When Mullvad connects/disconnects, Linux routing can change in a way that breaks
|
|
|
242
242
|
|
|
243
243
|
* Installs Python, FFmpeg, Mullvad CLI, and Deno
|
|
244
244
|
* Creates a virtualenv at `/opt/yt-dlp-mullvad/venv`
|
|
245
|
-
* Installs `ytp-dl==0.
|
|
245
|
+
* Installs `ytp-dl==0.7.0` + `yt-dlp[default]` + `gunicorn`
|
|
246
246
|
* Installs a policy-routing oneshot service to keep the public API reachable
|
|
247
247
|
* Sets up a systemd service on port 5000
|
|
248
248
|
* Runs Gunicorn with `gthread` (threaded) workers
|
|
@@ -258,7 +258,7 @@ Note: `gthread` is a built-in Gunicorn worker class (no extra Python dependency)
|
|
|
258
258
|
# - Installs Deno system-wide (JS runtime required for modern YouTube extraction via yt-dlp)
|
|
259
259
|
# - Configures policy routing so the public API stays reachable while Mullvad toggles
|
|
260
260
|
# - Creates a virtualenv at /opt/yt-dlp-mullvad/venv
|
|
261
|
-
# - Installs ytp-dl==0.
|
|
261
|
+
# - Installs ytp-dl==0.7.0 + yt-dlp[default] + gunicorn in that venv
|
|
262
262
|
# - Creates a systemd service ytp-dl-api.service on port 5000
|
|
263
263
|
#
|
|
264
264
|
# Mullvad connect/disconnect is handled per-job by downloader.py.
|
|
@@ -394,7 +394,7 @@ mkdir -p "${APP_DIR}"
|
|
|
394
394
|
python3 -m venv "${VENV_DIR}"
|
|
395
395
|
source "${VENV_DIR}/bin/activate"
|
|
396
396
|
pip install --upgrade pip
|
|
397
|
-
pip install "ytp-dl==0.
|
|
397
|
+
pip install "ytp-dl==0.7.0" "yt-dlp[default]" gunicorn
|
|
398
398
|
deactivate
|
|
399
399
|
|
|
400
400
|
echo "==> 3) API environment file (/etc/default/ytp-dl-api)"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
scripts/__init__.py,sha256=EbAplfCcyLD3Q_9sxemm6owCc5_UJv53vmlxy810p2s,152
|
|
2
|
+
scripts/api.py,sha256=Lwu-Bb1fg6fjKBECyNOtvt3Q6Kna5qwmYUT50X51vUk,10356
|
|
3
|
+
scripts/downloader.py,sha256=Zpjoxcm8HJ2_j7xMXQysfEKXanOHKtbLAFwuYkzqc4w,16049
|
|
4
|
+
ytp_dl-0.7.0.dist-info/METADATA,sha256=KG6IndrPKUI5xOzA3Ldx0iKTqBbwjoqAYDEIxVtXKdU,14547
|
|
5
|
+
ytp_dl-0.7.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
ytp_dl-0.7.0.dist-info/entry_points.txt,sha256=QqjqZZAEt3Y7RGrleqZ312sjjboUpbMLdo7qFxuCH30,48
|
|
7
|
+
ytp_dl-0.7.0.dist-info/top_level.txt,sha256=rmzd5mewlrJy4sT608KPib7sM7edoY75AeqJeY3SPB4,8
|
|
8
|
+
ytp_dl-0.7.0.dist-info/RECORD,,
|
ytp_dl-0.6.9.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
scripts/__init__.py,sha256=EbAplfCcyLD3Q_9sxemm6owCc5_UJv53vmlxy810p2s,152
|
|
2
|
-
scripts/api.py,sha256=cyLjHmelLwzh8-GOjqXsQdhm6wLX8bOkADZ_qU1naRQ,4331
|
|
3
|
-
scripts/downloader.py,sha256=vvHasu-41DGPDUzOTA4kz52tijTkaii1NnuU4cHQxg8,10825
|
|
4
|
-
ytp_dl-0.6.9.dist-info/METADATA,sha256=Z4H5XVsQIQgU3hFXdF-Lp2Yf6-sSzTjwpHAptrGeMYg,14547
|
|
5
|
-
ytp_dl-0.6.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
-
ytp_dl-0.6.9.dist-info/entry_points.txt,sha256=QqjqZZAEt3Y7RGrleqZ312sjjboUpbMLdo7qFxuCH30,48
|
|
7
|
-
ytp_dl-0.6.9.dist-info/top_level.txt,sha256=rmzd5mewlrJy4sT608KPib7sM7edoY75AeqJeY3SPB4,8
|
|
8
|
-
ytp_dl-0.6.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|