ytp-dl 0.6.9__py3-none-any.whl → 0.7.1__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 +171 -5
- scripts/downloader.py +174 -1
- {ytp_dl-0.6.9.dist-info → ytp_dl-0.7.1.dist-info}/METADATA +5 -5
- ytp_dl-0.7.1.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.1.dist-info}/WHEEL +0 -0
- {ytp_dl-0.6.9.dist-info → ytp_dl-0.7.1.dist-info}/entry_points.txt +0 -0
- {ytp_dl-0.6.9.dist-info → ytp_dl-0.7.1.dist-info}/top_level.txt +0 -0
scripts/api.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
5
6
|
import shutil
|
|
6
7
|
import tempfile
|
|
7
8
|
import time
|
|
8
9
|
from threading import BoundedSemaphore, Lock
|
|
10
|
+
from typing import Optional
|
|
9
11
|
|
|
10
|
-
from flask import Flask, request, send_file, jsonify
|
|
12
|
+
from flask import Flask, request, send_file, jsonify, Response, stream_with_context
|
|
11
13
|
|
|
12
|
-
from .downloader import validate_environment, download_video
|
|
14
|
+
from .downloader import validate_environment, download_video, download_video_stream
|
|
13
15
|
|
|
14
16
|
app = Flask(__name__)
|
|
15
17
|
|
|
@@ -65,8 +67,51 @@ def _release_job_slot() -> None:
|
|
|
65
67
|
_sem.release()
|
|
66
68
|
|
|
67
69
|
|
|
70
|
+
def _safe_job_id(raw: str) -> str:
|
|
71
|
+
"""Keep job ids filesystem-safe and predictable."""
|
|
72
|
+
s = (raw or "").strip()
|
|
73
|
+
if not s:
|
|
74
|
+
return str(int(time.time() * 1000))
|
|
75
|
+
# Only allow basic chars; everything else becomes "_"
|
|
76
|
+
out = []
|
|
77
|
+
for ch in s:
|
|
78
|
+
if ch.isalnum() or ch in ("-", "_"):
|
|
79
|
+
out.append(ch)
|
|
80
|
+
else:
|
|
81
|
+
out.append("_")
|
|
82
|
+
return "".join(out)[:120] or str(int(time.time() * 1000))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _write_result(job_dir: str, final_path: str) -> None:
|
|
86
|
+
"""Persist the final file path so /api/fetch can retrieve it."""
|
|
87
|
+
meta = {
|
|
88
|
+
"path": final_path,
|
|
89
|
+
"filename": os.path.basename(final_path),
|
|
90
|
+
"ts": time.time(),
|
|
91
|
+
}
|
|
92
|
+
with open(os.path.join(job_dir, "result.json"), "w", encoding="utf-8") as f:
|
|
93
|
+
json.dump(meta, f, ensure_ascii=False)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _read_result(job_dir: str) -> Optional[dict]:
|
|
97
|
+
"""Load persisted result metadata."""
|
|
98
|
+
p = os.path.join(job_dir, "result.json")
|
|
99
|
+
if not os.path.exists(p):
|
|
100
|
+
return None
|
|
101
|
+
try:
|
|
102
|
+
with open(p, "r", encoding="utf-8") as f:
|
|
103
|
+
return json.load(f)
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
68
108
|
@app.route("/api/download", methods=["POST"])
|
|
69
109
|
def handle_download():
|
|
110
|
+
"""
|
|
111
|
+
Legacy endpoint (unchanged):
|
|
112
|
+
- Runs yt-dlp to completion
|
|
113
|
+
- Returns the file as the HTTP response body
|
|
114
|
+
"""
|
|
70
115
|
_cleanup_stale_jobs()
|
|
71
116
|
|
|
72
117
|
if not _try_acquire_job_slot():
|
|
@@ -85,8 +130,6 @@ def handle_download():
|
|
|
85
130
|
data = request.get_json(force=True)
|
|
86
131
|
url = (data.get("url") or "").strip()
|
|
87
132
|
resolution = data.get("resolution")
|
|
88
|
-
|
|
89
|
-
# extension is now a "mode": mp3 | mp4 | best
|
|
90
133
|
extension = (data.get("extension") or "mp4").strip().lower()
|
|
91
134
|
|
|
92
135
|
if not url:
|
|
@@ -145,6 +188,129 @@ def handle_download():
|
|
|
145
188
|
return jsonify(error=f"Download failed: {str(e)}"), 500
|
|
146
189
|
|
|
147
190
|
|
|
191
|
+
@app.route("/api/download_sse", methods=["POST"])
|
|
192
|
+
def handle_download_sse():
|
|
193
|
+
"""
|
|
194
|
+
NEW:
|
|
195
|
+
- Streams raw yt-dlp stdout lines as SSE in real time.
|
|
196
|
+
- Persists the final path to job_dir/result.json.
|
|
197
|
+
- Does NOT stream the file in this response.
|
|
198
|
+
"""
|
|
199
|
+
_cleanup_stale_jobs()
|
|
200
|
+
|
|
201
|
+
if not _try_acquire_job_slot():
|
|
202
|
+
return jsonify(error="Server busy, try again later"), 503
|
|
203
|
+
|
|
204
|
+
released = False
|
|
205
|
+
|
|
206
|
+
def _release_once() -> None:
|
|
207
|
+
nonlocal released
|
|
208
|
+
if not released:
|
|
209
|
+
released = True
|
|
210
|
+
_release_job_slot()
|
|
211
|
+
|
|
212
|
+
data = request.get_json(force=True) or {}
|
|
213
|
+
url = (data.get("url") or "").strip()
|
|
214
|
+
resolution = data.get("resolution")
|
|
215
|
+
extension = (data.get("extension") or "mp4").strip().lower()
|
|
216
|
+
job_id = _safe_job_id(str(data.get("job_id") or ""))
|
|
217
|
+
|
|
218
|
+
if not url:
|
|
219
|
+
_release_once()
|
|
220
|
+
return jsonify(error="Missing 'url'"), 400
|
|
221
|
+
if extension not in _ALLOWED_EXTENSIONS:
|
|
222
|
+
_release_once()
|
|
223
|
+
return jsonify(error=f"Invalid 'extension'. Allowed: {sorted(_ALLOWED_EXTENSIONS)}"), 400
|
|
224
|
+
|
|
225
|
+
job_dir = os.path.join(BASE_DOWNLOAD_DIR, job_id)
|
|
226
|
+
os.makedirs(job_dir, exist_ok=True)
|
|
227
|
+
|
|
228
|
+
def gen():
|
|
229
|
+
try:
|
|
230
|
+
# download_video_stream is a generator that yields yt-dlp lines and
|
|
231
|
+
# returns final_path via StopIteration.value
|
|
232
|
+
g = download_video_stream(
|
|
233
|
+
url=url,
|
|
234
|
+
resolution=resolution,
|
|
235
|
+
extension=extension,
|
|
236
|
+
out_dir=job_dir,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
final_path: Optional[str] = None
|
|
240
|
+
|
|
241
|
+
while True:
|
|
242
|
+
try:
|
|
243
|
+
line = next(g)
|
|
244
|
+
except StopIteration as si:
|
|
245
|
+
final_path = si.value # type: ignore[assignment]
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
line = (line or "").strip()
|
|
249
|
+
if not line:
|
|
250
|
+
continue
|
|
251
|
+
yield f"data: {line}\n\n"
|
|
252
|
+
|
|
253
|
+
if not final_path or not os.path.exists(final_path):
|
|
254
|
+
raise RuntimeError("Download completed but output file not found")
|
|
255
|
+
|
|
256
|
+
_write_result(job_dir, final_path)
|
|
257
|
+
|
|
258
|
+
# Marker consumed by your Render app (not forwarded to the browser).
|
|
259
|
+
yield f"data: [vps_ready] {os.path.basename(final_path)}\n\n"
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
msg = str(e)
|
|
263
|
+
yield f"data: VPS error: {msg}\n\n"
|
|
264
|
+
finally:
|
|
265
|
+
_release_once()
|
|
266
|
+
|
|
267
|
+
resp = Response(stream_with_context(gen()), content_type="text/event-stream")
|
|
268
|
+
resp.headers["Cache-Control"] = "no-cache"
|
|
269
|
+
resp.headers["X-Accel-Buffering"] = "no"
|
|
270
|
+
return resp
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@app.route("/api/fetch/<job_id>", methods=["GET"])
|
|
274
|
+
def fetch_job(job_id: str):
|
|
275
|
+
"""
|
|
276
|
+
NEW:
|
|
277
|
+
- Returns the completed file for a prior /api/download_sse job.
|
|
278
|
+
- Deletes the job dir after the client finishes consuming the response.
|
|
279
|
+
"""
|
|
280
|
+
job_id = _safe_job_id(job_id)
|
|
281
|
+
job_dir = os.path.join(BASE_DOWNLOAD_DIR, job_id)
|
|
282
|
+
meta = _read_result(job_dir)
|
|
283
|
+
if not meta:
|
|
284
|
+
return jsonify(error="Job not found or not ready"), 404
|
|
285
|
+
|
|
286
|
+
path = (meta.get("path") or "").strip()
|
|
287
|
+
if not path:
|
|
288
|
+
return jsonify(error="Job result missing path"), 500
|
|
289
|
+
|
|
290
|
+
# Prevent path traversal: ensure file lives inside job_dir
|
|
291
|
+
try:
|
|
292
|
+
job_dir_abs = os.path.abspath(job_dir)
|
|
293
|
+
path_abs = os.path.abspath(path)
|
|
294
|
+
if os.path.commonpath([job_dir_abs, path_abs]) != job_dir_abs:
|
|
295
|
+
return jsonify(error="Invalid job result path"), 500
|
|
296
|
+
except Exception:
|
|
297
|
+
return jsonify(error="Invalid job result path"), 500
|
|
298
|
+
|
|
299
|
+
if not os.path.exists(path):
|
|
300
|
+
return jsonify(error="File not found"), 404
|
|
301
|
+
|
|
302
|
+
response = send_file(path, as_attachment=True)
|
|
303
|
+
|
|
304
|
+
def _cleanup() -> None:
|
|
305
|
+
try:
|
|
306
|
+
shutil.rmtree(job_dir, ignore_errors=True)
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
response.call_on_close(_cleanup)
|
|
311
|
+
return response
|
|
312
|
+
|
|
313
|
+
|
|
148
314
|
@app.route("/healthz", methods=["GET"])
|
|
149
315
|
def healthz():
|
|
150
316
|
with _in_use_lock:
|
|
@@ -159,4 +325,4 @@ def main():
|
|
|
159
325
|
|
|
160
326
|
|
|
161
327
|
if __name__ == "__main__":
|
|
162
|
-
main()
|
|
328
|
+
main()
|
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 Optional, List, Tuple, Deque, Generator
|
|
10
11
|
|
|
11
12
|
# =========================
|
|
12
13
|
# Config / constants
|
|
@@ -278,6 +279,100 @@ def _download_with_format(
|
|
|
278
279
|
raise RuntimeError(f"Download completed but output file not found (format: {fmt})\n{tail}")
|
|
279
280
|
|
|
280
281
|
|
|
282
|
+
def _download_with_format_stream(
|
|
283
|
+
url: str,
|
|
284
|
+
out_dir: str,
|
|
285
|
+
fmt: str,
|
|
286
|
+
merge_output_format: Optional[str] = None,
|
|
287
|
+
extract_mp3: bool = False,
|
|
288
|
+
) -> Generator[str, None, str]:
|
|
289
|
+
"""
|
|
290
|
+
Stream yt-dlp output line-by-line (stdout+stderr merged),
|
|
291
|
+
and return the final output path via StopIteration.value.
|
|
292
|
+
"""
|
|
293
|
+
out_dir = os.path.abspath(out_dir)
|
|
294
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
295
|
+
|
|
296
|
+
out_tpl = os.path.join(out_dir, "%(title)s.%(ext)s")
|
|
297
|
+
|
|
298
|
+
argv = [
|
|
299
|
+
YTDLP_BIN,
|
|
300
|
+
"-f", fmt,
|
|
301
|
+
*(_common_flags()),
|
|
302
|
+
"--output", out_tpl,
|
|
303
|
+
|
|
304
|
+
# Make progress lines flush as newline-terminated output
|
|
305
|
+
"--progress",
|
|
306
|
+
"--newline",
|
|
307
|
+
|
|
308
|
+
# Ensure we can reliably obtain the post-processed final path
|
|
309
|
+
"--print", "after_move:filepath",
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
if extract_mp3:
|
|
313
|
+
argv.extend(["--extract-audio", "--audio-format", "mp3"])
|
|
314
|
+
|
|
315
|
+
if merge_output_format:
|
|
316
|
+
argv.extend(["--merge-output-format", merge_output_format])
|
|
317
|
+
|
|
318
|
+
argv.append(url)
|
|
319
|
+
|
|
320
|
+
proc = subprocess.Popen(
|
|
321
|
+
argv,
|
|
322
|
+
stdout=subprocess.PIPE,
|
|
323
|
+
stderr=subprocess.STDOUT,
|
|
324
|
+
text=True,
|
|
325
|
+
bufsize=1,
|
|
326
|
+
universal_newlines=True,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
assert proc.stdout is not None
|
|
330
|
+
|
|
331
|
+
tail_buf: Deque[str] = deque(maxlen=_MAX_ERR_LINES)
|
|
332
|
+
final_path: Optional[str] = None
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
for raw in iter(proc.stdout.readline, ""):
|
|
336
|
+
line = (raw or "").rstrip("\n")
|
|
337
|
+
if not line:
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
tail_buf.append(line)
|
|
341
|
+
|
|
342
|
+
# Capture after_move:filepath lines (absolute paths under out_dir)
|
|
343
|
+
cand = line.strip().strip("'\"")
|
|
344
|
+
try:
|
|
345
|
+
if os.path.isabs(cand):
|
|
346
|
+
cand_abs = os.path.abspath(cand)
|
|
347
|
+
if os.path.commonpath([out_dir, cand_abs]) == out_dir:
|
|
348
|
+
final_path = cand_abs
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
yield line
|
|
353
|
+
|
|
354
|
+
finally:
|
|
355
|
+
try:
|
|
356
|
+
proc.stdout.close()
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
rc = proc.wait()
|
|
361
|
+
|
|
362
|
+
if rc != 0:
|
|
363
|
+
raise RuntimeError(f"yt-dlp failed (format: {fmt})\n{_tail('\n'.join(tail_buf))}")
|
|
364
|
+
|
|
365
|
+
if final_path and os.path.exists(final_path):
|
|
366
|
+
return os.path.abspath(final_path)
|
|
367
|
+
|
|
368
|
+
# Fallback: newest non-temp file in out_dir
|
|
369
|
+
fallback = _extract_final_path("\n".join(tail_buf), out_dir)
|
|
370
|
+
if fallback and os.path.exists(fallback):
|
|
371
|
+
return os.path.abspath(fallback)
|
|
372
|
+
|
|
373
|
+
raise RuntimeError(f"Download completed but output file not found (format: {fmt})\n{_tail('\n'.join(tail_buf))}")
|
|
374
|
+
|
|
375
|
+
|
|
281
376
|
def _fmt_mp4_apple_safe(cap: int) -> str:
|
|
282
377
|
# Always pick the best Apple-safe MP4/H.264 + M4A/AAC up to cap.
|
|
283
378
|
return (
|
|
@@ -361,3 +456,81 @@ def download_video(
|
|
|
361
456
|
finally:
|
|
362
457
|
if _mullvad_present():
|
|
363
458
|
_run_argv(["mullvad", "disconnect"], check=False)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def download_video_stream(
|
|
462
|
+
url: str,
|
|
463
|
+
resolution: int | None = 1080,
|
|
464
|
+
extension: Optional[str] = None,
|
|
465
|
+
out_dir: str = DEFAULT_OUT_DIR,
|
|
466
|
+
) -> Generator[str, None, str]:
|
|
467
|
+
"""
|
|
468
|
+
Stream raw yt-dlp output lines and return the final file path.
|
|
469
|
+
Mirrors download_video(), but line-streaming instead of capture.
|
|
470
|
+
"""
|
|
471
|
+
if not url:
|
|
472
|
+
raise RuntimeError("Missing URL")
|
|
473
|
+
|
|
474
|
+
out_dir = os.path.abspath(out_dir)
|
|
475
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
476
|
+
|
|
477
|
+
validate_environment()
|
|
478
|
+
|
|
479
|
+
require_mullvad_login()
|
|
480
|
+
mullvad_connect(MULLVAD_LOCATION)
|
|
481
|
+
if not mullvad_wait_connected():
|
|
482
|
+
raise RuntimeError("Mullvad connection failed")
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
mode = (extension or "mp4").lower().strip()
|
|
486
|
+
|
|
487
|
+
if mode == "mp3":
|
|
488
|
+
g = _download_with_format_stream(
|
|
489
|
+
url=url,
|
|
490
|
+
out_dir=out_dir,
|
|
491
|
+
fmt="bestaudio",
|
|
492
|
+
merge_output_format=None,
|
|
493
|
+
extract_mp3=True,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
else:
|
|
497
|
+
cap = int(resolution or 1080)
|
|
498
|
+
|
|
499
|
+
if mode == "best":
|
|
500
|
+
# Try best; on failure fall back to Apple-safe mp4 (same logic as download_video)
|
|
501
|
+
try:
|
|
502
|
+
g = _download_with_format_stream(
|
|
503
|
+
url=url,
|
|
504
|
+
out_dir=out_dir,
|
|
505
|
+
fmt=_fmt_best(cap),
|
|
506
|
+
merge_output_format=None,
|
|
507
|
+
extract_mp3=False,
|
|
508
|
+
)
|
|
509
|
+
except Exception:
|
|
510
|
+
g = _download_with_format_stream(
|
|
511
|
+
url=url,
|
|
512
|
+
out_dir=out_dir,
|
|
513
|
+
fmt=_fmt_mp4_apple_safe(cap),
|
|
514
|
+
merge_output_format="mp4",
|
|
515
|
+
extract_mp3=False,
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
g = _download_with_format_stream(
|
|
519
|
+
url=url,
|
|
520
|
+
out_dir=out_dir,
|
|
521
|
+
fmt=_fmt_mp4_apple_safe(cap),
|
|
522
|
+
merge_output_format="mp4",
|
|
523
|
+
extract_mp3=False,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Forward all lines; return final path via StopIteration.value
|
|
527
|
+
while True:
|
|
528
|
+
try:
|
|
529
|
+
line = next(g)
|
|
530
|
+
except StopIteration as si:
|
|
531
|
+
return si.value # final path
|
|
532
|
+
yield line
|
|
533
|
+
|
|
534
|
+
finally:
|
|
535
|
+
if _mullvad_present():
|
|
536
|
+
_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.1
|
|
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.1 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.1` + `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.1 + 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.1" "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=dqVcS1niL2ziv3w3WipiDH8T0peNlSzRCBfm1xxR4N0,9562
|
|
3
|
+
scripts/downloader.py,sha256=Fo8srnATM9Eee0yWvGsr5LKp6Zg8KTpsW7TFk24YZlg,15916
|
|
4
|
+
ytp_dl-0.7.1.dist-info/METADATA,sha256=8WsJ-TM4orFucsTAEck6S95dAuh5P2eRLVpurKlFhDk,14547
|
|
5
|
+
ytp_dl-0.7.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
ytp_dl-0.7.1.dist-info/entry_points.txt,sha256=QqjqZZAEt3Y7RGrleqZ312sjjboUpbMLdo7qFxuCH30,48
|
|
7
|
+
ytp_dl-0.7.1.dist-info/top_level.txt,sha256=rmzd5mewlrJy4sT608KPib7sM7edoY75AeqJeY3SPB4,8
|
|
8
|
+
ytp_dl-0.7.1.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
|