ytp-dl 0.6.6__py3-none-any.whl → 0.6.8__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 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_stream
13
15
 
14
16
  app = Flask(__name__)
15
17
 
@@ -65,8 +67,49 @@ def _release_job_slot() -> None:
65
67
  _sem.release()
66
68
 
67
69
 
70
+ def _job_meta_path(job_dir: str) -> str:
71
+ return os.path.join(job_dir, "job.json")
72
+
73
+
74
+ def _write_job_meta(job_dir: str, meta: dict) -> None:
75
+ try:
76
+ with open(_job_meta_path(job_dir), "w", encoding="utf-8") as f:
77
+ json.dump(meta, f, ensure_ascii=False)
78
+ except Exception:
79
+ pass
80
+
81
+
82
+ def _read_job_meta(job_dir: str) -> Optional[dict]:
83
+ path = _job_meta_path(job_dir)
84
+ if not os.path.exists(path):
85
+ return None
86
+ try:
87
+ with open(path, "r", encoding="utf-8") as f:
88
+ return json.load(f)
89
+ except Exception:
90
+ return None
91
+
92
+
93
+ def _sse_message(data: str) -> str:
94
+ # one "message" event
95
+ return f"data: {data}\n\n"
96
+
97
+
98
+ def _sse_event(event_name: str, data: str) -> str:
99
+ # custom event type
100
+ return f"event: {event_name}\ndata: {data}\n\n"
101
+
102
+
68
103
  @app.route("/api/download", methods=["POST"])
69
104
  def handle_download():
105
+ """
106
+ Streams yt-dlp logs via SSE (real-time), then emits a final custom `result` event:
107
+ event: result
108
+ data: {"job_id":"ytpdl_xxx","filename":"file.mp4"}
109
+
110
+ The finished file is retrieved separately via:
111
+ GET /api/file/<job_id>
112
+ """
70
113
  _cleanup_stale_jobs()
71
114
 
72
115
  if not _try_acquire_job_slot():
@@ -100,43 +143,88 @@ def handle_download():
100
143
  ), 400
101
144
 
102
145
  job_dir = tempfile.mkdtemp(prefix="ytpdl_", dir=BASE_DOWNLOAD_DIR)
146
+ job_id = os.path.basename(job_dir)
103
147
 
104
- # yt-dlp work (guarded by semaphore)
105
- filename = download_video(
106
- url=url,
107
- resolution=resolution,
108
- extension=extension,
109
- out_dir=job_dir,
110
- )
111
-
112
- if not (filename and os.path.exists(filename)):
113
- raise RuntimeError("Download failed")
114
-
115
- # Release slot as soon as yt-dlp is done.
116
- _release_once()
117
-
118
- response = send_file(filename, as_attachment=True)
119
-
120
- # Cleanup directory after client finishes consuming the response.
121
- def _cleanup() -> None:
148
+ def stream():
149
+ nonlocal job_dir
122
150
  try:
151
+ # ---- yt-dlp streamed logs ----
152
+ # download_video_stream yields yt-dlp stdout lines.
153
+ filename_path = yield from download_video_stream(
154
+ url=url,
155
+ resolution=resolution,
156
+ extension=extension,
157
+ out_dir=job_dir,
158
+ )
159
+
160
+ if not (filename_path and os.path.exists(filename_path)):
161
+ yield _sse_message("ERROR: Download failed (file missing).")
162
+ yield _sse_event(
163
+ "error",
164
+ json.dumps({"error": "Download failed (file missing)"}),
165
+ )
166
+ return
167
+
168
+ # Release slot as soon as yt-dlp is done.
169
+ _release_once()
170
+
171
+ out_name = os.path.basename(filename_path)
172
+
173
+ # Persist meta for /api/file/<job_id>
174
+ _write_job_meta(
175
+ job_dir,
176
+ {
177
+ "job_id": job_id,
178
+ "filename": out_name,
179
+ "file_path": filename_path,
180
+ "created_at": time.time(),
181
+ },
182
+ )
183
+
184
+ # Result event (custom type) so your browser log UI won't display it
185
+ yield _sse_event(
186
+ "result",
187
+ json.dumps({"job_id": job_id, "filename": out_name}),
188
+ )
189
+
190
+ # keep-alive tail
191
+ yield _sse_message("All downloads complete.")
192
+ return
193
+
194
+ except RuntimeError as e:
123
195
  if job_dir:
124
196
  shutil.rmtree(job_dir, ignore_errors=True)
125
- except Exception:
126
- pass
197
+ _release_once()
198
+
199
+ msg = str(e)
200
+ # make it visible in logs
201
+ yield _sse_message(f"ERROR: {msg}")
202
+ # also machine-readable
203
+ code = 503 if "Mullvad not logged in" in msg else 500
204
+ yield _sse_event("error", json.dumps({"error": msg, "code": code}))
205
+ return
206
+
207
+ except GeneratorExit:
208
+ # Client disconnected mid-stream; best-effort cleanup.
209
+ if job_dir:
210
+ shutil.rmtree(job_dir, ignore_errors=True)
211
+ _release_once()
212
+ raise
127
213
 
128
- response.call_on_close(_cleanup)
129
- return response
214
+ except Exception as e:
215
+ if job_dir:
216
+ shutil.rmtree(job_dir, ignore_errors=True)
217
+ _release_once()
130
218
 
131
- except RuntimeError as e:
132
- if job_dir:
133
- shutil.rmtree(job_dir, ignore_errors=True)
134
- _release_once()
219
+ msg = f"Download failed: {str(e)}"
220
+ yield _sse_message(f"ERROR: {msg}")
221
+ yield _sse_event("error", json.dumps({"error": msg, "code": 500}))
222
+ return
135
223
 
136
- msg = str(e)
137
- if "Mullvad not logged in" in msg:
138
- return jsonify(error=msg), 503
139
- return jsonify(error=f"Download failed: {msg}"), 500
224
+ resp = Response(stream_with_context(stream()), content_type="text/event-stream")
225
+ resp.headers["Cache-Control"] = "no-cache"
226
+ resp.headers["X-Accel-Buffering"] = "no"
227
+ return resp
140
228
 
141
229
  except Exception as e:
142
230
  if job_dir:
@@ -145,6 +233,42 @@ def handle_download():
145
233
  return jsonify(error=f"Download failed: {str(e)}"), 500
146
234
 
147
235
 
236
+ @app.route("/api/file/<job_id>", methods=["GET"])
237
+ def fetch_file(job_id: str):
238
+ """
239
+ After /api/download SSE completes with `event: result`,
240
+ the caller fetches the finished file here.
241
+ """
242
+ _cleanup_stale_jobs()
243
+
244
+ job_dir = os.path.join(BASE_DOWNLOAD_DIR, job_id)
245
+ if not os.path.isdir(job_dir):
246
+ return jsonify(error="Job not found"), 404
247
+
248
+ meta = _read_job_meta(job_dir)
249
+ if not meta:
250
+ return jsonify(error="Job metadata missing"), 404
251
+
252
+ file_path = meta.get("file_path")
253
+ filename = meta.get("filename") or (os.path.basename(file_path) if file_path else None)
254
+
255
+ if not file_path or not os.path.exists(file_path):
256
+ shutil.rmtree(job_dir, ignore_errors=True)
257
+ return jsonify(error="File not found"), 404
258
+
259
+ response = send_file(file_path, as_attachment=True, download_name=filename)
260
+
261
+ # Cleanup directory after client finishes consuming the response.
262
+ def _cleanup() -> None:
263
+ try:
264
+ shutil.rmtree(job_dir, ignore_errors=True)
265
+ except Exception:
266
+ pass
267
+
268
+ response.call_on_close(_cleanup)
269
+ return response
270
+
271
+
148
272
  @app.route("/healthz", methods=["GET"])
149
273
  def healthz():
150
274
  with _in_use_lock:
scripts/downloader.py CHANGED
@@ -6,7 +6,7 @@ import shlex
6
6
  import shutil
7
7
  import subprocess
8
8
  import time
9
- from typing import Optional, List, Tuple
9
+ from typing import Optional, List, Tuple, Generator
10
10
 
11
11
  # =========================
12
12
  # Config / constants
@@ -60,11 +60,6 @@ def _tail(out: str) -> str:
60
60
  return txt.strip()
61
61
 
62
62
 
63
- def _is_youtube_url(url: str) -> bool:
64
- u = (url or "").lower()
65
- return any(h in u for h in ("youtube.com", "youtu.be", "youtube-nocookie.com"))
66
-
67
-
68
63
  # =========================
69
64
  # Environment / Mullvad
70
65
  # =========================
@@ -135,85 +130,25 @@ def _common_flags() -> List[str]:
135
130
  "--user-agent", MODERN_UA,
136
131
  "--no-cache-dir",
137
132
  "--ignore-config",
138
- "--embed-metadata",
139
133
  "--sleep-interval", "1",
140
134
  ]
141
135
 
142
136
 
143
- def _extract_final_path(stdout: str, out_dir: str) -> Optional[str]:
144
- """
145
- Robustly derive the final output file path from yt-dlp output.
146
-
147
- Priority:
148
- 1) --print after_move:filepath lines (absolute paths)
149
- 2) [Merger] Merging formats into "..."
150
- 3) Any Destination: lines that still exist
151
- 4) Newest non-temp file in out_dir
152
- """
153
- candidates: List[str] = []
154
- out_dir = os.path.abspath(out_dir)
155
-
156
- for raw in (stdout or "").splitlines():
157
- line = (raw or "").strip()
158
- if not line:
159
- continue
160
-
161
- # 1) --print after_move:filepath (usually an absolute path)
162
- if os.path.isabs(line) and line.startswith(out_dir):
163
- candidates.append(line.strip("'\""))
164
- continue
137
+ def _fmt_mp4_apple_safe(cap: int) -> str:
138
+ # Always pick the best Apple-safe MP4/H.264 + M4A/AAC up to cap.
139
+ return (
140
+ f"bv*[height<={cap}][ext=mp4][vcodec~='^(avc1|h264)']"
141
+ f"+ba[ext=m4a][acodec~='^mp4a']"
142
+ f"/b[height<={cap}][ext=mp4][vcodec~='^(avc1|h264)'][acodec~='^mp4a']"
143
+ )
165
144
 
166
- # 2) Merger line: ... into "path"
167
- if "Merging formats into" in line and "\"" in line:
168
- try:
169
- merged = line.split("Merging formats into", 1)[1].strip()
170
- if merged.startswith("\"") and merged.endswith("\""):
171
- merged = merged[1:-1]
172
- else:
173
- if merged.startswith("\""):
174
- merged = merged.split("\"", 2)[1]
175
- if merged:
176
- if not os.path.isabs(merged):
177
- merged = os.path.join(out_dir, merged)
178
- candidates.append(merged.strip("'\""))
179
- except Exception:
180
- pass
181
- continue
182
145
 
183
- # 3) Destination lines (download/extractaudio)
184
- if "Destination:" in line:
185
- try:
186
- p = line.split("Destination:", 1)[1].strip().strip("'\"")
187
- if p and not os.path.isabs(p):
188
- p = os.path.join(out_dir, p)
189
- if p:
190
- candidates.append(p)
191
- except Exception:
192
- pass
193
- continue
194
-
195
- # already downloaded
196
- if "] " in line and " has already been downloaded" in line:
197
- try:
198
- p = (
199
- line.split("] ", 1)[1]
200
- .split(" has already been downloaded", 1)[0]
201
- .strip()
202
- .strip("'\"")
203
- )
204
- if p and not os.path.isabs(p):
205
- p = os.path.join(out_dir, p)
206
- if p:
207
- candidates.append(p)
208
- except Exception:
209
- pass
146
+ def _fmt_best(cap: int) -> str:
147
+ # Best overall up to cap (can yield webm/mkv/etc).
148
+ return f"bv*[height<={cap}]+ba/b[height<={cap}]"
210
149
 
211
- # Prefer existing, newest candidate (reverse traversal)
212
- for p in reversed(candidates):
213
- if p and os.path.exists(p):
214
- return p
215
150
 
216
- # 4) Fallback: newest non-temp file in out_dir
151
+ def _newest_non_temp_file(out_dir: str) -> Optional[str]:
217
152
  try:
218
153
  best_path = None
219
154
  best_mtime = -1.0
@@ -227,80 +162,129 @@ def _extract_final_path(stdout: str, out_dir: str) -> Optional[str]:
227
162
  if mt > best_mtime:
228
163
  best_mtime = mt
229
164
  best_path = full
230
- if best_path:
231
- return best_path
165
+ return best_path
232
166
  except Exception:
233
- pass
234
-
235
- return None
167
+ return None
236
168
 
237
169
 
238
- def _download_with_format(
170
+ def _download_with_format_stream(
171
+ *,
239
172
  url: str,
240
173
  out_dir: str,
241
174
  fmt: str,
242
175
  merge_output_format: Optional[str] = None,
243
176
  extract_mp3: bool = False,
244
- ) -> str:
177
+ ) -> Generator[str, None, str]:
178
+ """
179
+ Stream yt-dlp stdout lines (same style as local: --progress --newline),
180
+ while capturing final output path reliably.
181
+
182
+ Returns absolute file path (generator return value).
183
+ """
245
184
  out_dir = os.path.abspath(out_dir)
246
185
  os.makedirs(out_dir, exist_ok=True)
247
186
 
248
187
  out_tpl = os.path.join(out_dir, "%(title)s.%(ext)s")
249
188
 
250
- argv = [
189
+ argv: List[str] = [
251
190
  YTDLP_BIN,
191
+ url,
192
+ "--progress",
193
+ "--newline",
194
+ "--continue",
252
195
  "-f", fmt,
253
196
  *(_common_flags()),
254
197
  "--output", out_tpl,
255
- # Ensure we can reliably pick the final output path.
198
+ # Absolute final path for internal capture (we do NOT emit this line).
256
199
  "--print", "after_move:filepath",
200
+ # Local parity signal:
201
+ "--print", "after_move:[download_complete] %(title)s.%(ext)s",
257
202
  ]
258
203
 
259
204
  if extract_mp3:
260
- # Force audio extraction to MP3 (requires ffmpeg)
261
- argv.extend(["--extract-audio", "--audio-format", "mp3"])
205
+ argv.extend(
206
+ [
207
+ "--extract-audio",
208
+ "--audio-format", "mp3",
209
+ "--audio-quality", "0",
210
+ "--embed-thumbnail",
211
+ "--add-metadata",
212
+ ]
213
+ )
262
214
 
263
- # Only force merge container when we actually want MP4 output.
264
215
  if merge_output_format:
265
216
  argv.extend(["--merge-output-format", merge_output_format])
266
217
 
267
- argv.append(url)
218
+ proc = subprocess.Popen(
219
+ argv,
220
+ stdout=subprocess.PIPE,
221
+ stderr=subprocess.STDOUT,
222
+ text=True,
223
+ bufsize=1,
224
+ universal_newlines=True,
225
+ )
268
226
 
269
- rc, out = _run_argv_capture(argv)
270
- path = _extract_final_path(out, out_dir)
227
+ final_path: Optional[str] = None
228
+ tail_lines: List[str] = []
271
229
 
272
- if path and os.path.exists(path):
273
- return os.path.abspath(path)
230
+ try:
231
+ assert proc.stdout is not None
232
+ for raw in iter(proc.stdout.readline, ""):
233
+ line = (raw or "").rstrip("\n").rstrip("\r")
234
+ if not line:
235
+ continue
274
236
 
275
- tail = _tail(out)
276
- if rc != 0:
277
- raise RuntimeError(f"yt-dlp failed (format: {fmt})\n{tail}")
278
- raise RuntimeError(f"Download completed but output file not found (format: {fmt})\n{tail}")
237
+ # Capture absolute final path from after_move:filepath (do not emit to logs)
238
+ if os.path.isabs(line) and line.startswith(out_dir):
239
+ final_path = line.strip("'\"")
240
+ continue
279
241
 
242
+ # Keep a small tail buffer for error reporting
243
+ tail_lines.append(line)
244
+ if len(tail_lines) > _MAX_ERR_LINES:
245
+ tail_lines.pop(0)
280
246
 
281
- def _fmt_mp4_apple_safe(cap: int) -> str:
282
- # Always pick the best Apple-safe MP4/H.264 + M4A/AAC up to cap.
283
- return (
284
- f"bv*[height<={cap}][ext=mp4][vcodec~='^(avc1|h264)']"
285
- f"+ba[ext=m4a][acodec~='^mp4a']"
286
- f"/b[height<={cap}][ext=mp4][vcodec~='^(avc1|h264)'][acodec~='^mp4a']"
287
- )
247
+ # Emit everything else (yt-dlp progress + [download_complete] line)
248
+ yield line
288
249
 
250
+ proc.wait()
251
+ finally:
252
+ try:
253
+ if proc.stdout:
254
+ proc.stdout.close()
255
+ except Exception:
256
+ pass
257
+
258
+ if proc.returncode != 0:
259
+ tail = "\n".join(tail_lines)
260
+ tail = _tail(tail)
261
+ raise RuntimeError(f"yt-dlp failed (format: {fmt})\n{tail}")
289
262
 
290
- def _fmt_best(cap: int) -> str:
291
- # Best overall up to cap (can yield webm/mkv/etc).
292
- return f"bv*[height<={cap}]+ba/b[height<={cap}]"
263
+ # Resolve final path
264
+ if final_path and os.path.exists(final_path):
265
+ return os.path.abspath(final_path)
266
+
267
+ # Fallback: newest output in out_dir
268
+ newest = _newest_non_temp_file(out_dir)
269
+ if newest and os.path.exists(newest):
270
+ return os.path.abspath(newest)
271
+
272
+ tail = _tail("\n".join(tail_lines))
273
+ raise RuntimeError(f"Download completed but output file not found (format: {fmt})\n{tail}")
293
274
 
294
275
 
295
276
  # =========================
296
- # Public API
277
+ # Public APIs
297
278
  # =========================
298
- def download_video(
279
+ def download_video_stream(
299
280
  url: str,
300
281
  resolution: int | None = 1080,
301
282
  extension: Optional[str] = None,
302
283
  out_dir: str = DEFAULT_OUT_DIR,
303
- ) -> str:
284
+ ) -> Generator[str, None, str]:
285
+ """
286
+ Streams yt-dlp logs and returns final file path (generator return value).
287
+ """
304
288
  if not url:
305
289
  raise RuntimeError("Missing URL")
306
290
 
@@ -318,46 +302,68 @@ def download_video(
318
302
  mode = (extension or "mp4").lower().strip()
319
303
 
320
304
  if mode == "mp3":
321
- # bestaudio -> ffmpeg -> mp3 (post-processed by yt-dlp)
322
- return _download_with_format(
305
+ return (yield from _download_with_format_stream(
323
306
  url=url,
324
307
  out_dir=out_dir,
325
308
  fmt="bestaudio",
326
309
  merge_output_format=None,
327
310
  extract_mp3=True,
328
- )
311
+ ))
329
312
 
330
313
  cap = int(resolution or 1080)
331
314
 
332
315
  if mode == "best":
333
- # Try best first (may produce webm/mkv/etc).
316
+ # Try best first (may produce webm/mkv/etc). If it fails, fall back to Apple-safe MP4.
334
317
  try:
335
- return _download_with_format(
318
+ return (yield from _download_with_format_stream(
336
319
  url=url,
337
320
  out_dir=out_dir,
338
321
  fmt=_fmt_best(cap),
339
322
  merge_output_format=None,
340
323
  extract_mp3=False,
341
- )
324
+ ))
342
325
  except Exception:
343
- # If best fails for any reason, fall back to Apple-safe MP4.
344
- return _download_with_format(
326
+ return (yield from _download_with_format_stream(
345
327
  url=url,
346
328
  out_dir=out_dir,
347
329
  fmt=_fmt_mp4_apple_safe(cap),
348
330
  merge_output_format="mp4",
349
331
  extract_mp3=False,
350
- )
332
+ ))
351
333
 
352
- # Default / "mp4" mode: force Apple-safe MP4 up to cap.
353
- return _download_with_format(
334
+ # Default / "mp4" mode
335
+ return (yield from _download_with_format_stream(
354
336
  url=url,
355
337
  out_dir=out_dir,
356
338
  fmt=_fmt_mp4_apple_safe(cap),
357
339
  merge_output_format="mp4",
358
340
  extract_mp3=False,
359
- )
341
+ ))
360
342
 
361
343
  finally:
362
344
  if _mullvad_present():
363
345
  _run_argv(["mullvad", "disconnect"], check=False)
346
+
347
+
348
+ def download_video(
349
+ url: str,
350
+ resolution: int | None = 1080,
351
+ extension: Optional[str] = None,
352
+ out_dir: str = DEFAULT_OUT_DIR,
353
+ ) -> str:
354
+ """
355
+ Backward-compatible non-streaming wrapper.
356
+ """
357
+ gen = download_video_stream(
358
+ url=url,
359
+ resolution=resolution,
360
+ extension=extension,
361
+ out_dir=out_dir,
362
+ )
363
+ try:
364
+ for _ in gen:
365
+ pass
366
+ except StopIteration as si:
367
+ return si.value # type: ignore[attr-defined]
368
+ # Should never happen
369
+ raise RuntimeError("Download failed (no result)")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ytp-dl
3
- Version: 0.6.6
3
+ Version: 0.6.8
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.6.5 yt-dlp[default]
60
+ pip install ytp-dl==0.6.8 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.6.5` + `yt-dlp[default]` + `gunicorn`
245
+ * Installs `ytp-dl==0.6.8` + `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.6.5 + yt-dlp[default] + gunicorn in that venv
261
+ # - Installs ytp-dl==0.6.8 + 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.6.5" "yt-dlp[default]" gunicorn
397
+ pip install "ytp-dl==0.6.8" "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=EMpD_vRX5FZQ-ICIxLuRqJxitvlbi1VnUflWMC4yvmw,8560
3
+ scripts/downloader.py,sha256=LT7ANnpf7DRgVuRdNynqIXMKfKaeohy508OCXmltLtA,10529
4
+ ytp_dl-0.6.8.dist-info/METADATA,sha256=g5Q33WgF9ZBJYLdFYSt2cAiyl9QPSD1YpOeXcFUw628,14547
5
+ ytp_dl-0.6.8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ ytp_dl-0.6.8.dist-info/entry_points.txt,sha256=QqjqZZAEt3Y7RGrleqZ312sjjboUpbMLdo7qFxuCH30,48
7
+ ytp_dl-0.6.8.dist-info/top_level.txt,sha256=rmzd5mewlrJy4sT608KPib7sM7edoY75AeqJeY3SPB4,8
8
+ ytp_dl-0.6.8.dist-info/RECORD,,
@@ -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.6.dist-info/METADATA,sha256=nFKue1M7QDCVK2Uf0FeN7Le6M2pIrGbkb6xcl4V2ojg,14547
5
- ytp_dl-0.6.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
- ytp_dl-0.6.6.dist-info/entry_points.txt,sha256=QqjqZZAEt3Y7RGrleqZ312sjjboUpbMLdo7qFxuCH30,48
7
- ytp_dl-0.6.6.dist-info/top_level.txt,sha256=rmzd5mewlrJy4sT608KPib7sM7edoY75AeqJeY3SPB4,8
8
- ytp_dl-0.6.6.dist-info/RECORD,,
File without changes