ytp-dl 0.6.8__py3-none-any.whl → 0.6.9__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,17 +1,15 @@
1
1
  #!/usr/bin/env python3
2
2
  from __future__ import annotations
3
3
 
4
- import json
5
4
  import os
6
5
  import shutil
7
6
  import tempfile
8
7
  import time
9
8
  from threading import BoundedSemaphore, Lock
10
- from typing import Optional
11
9
 
12
- from flask import Flask, request, send_file, jsonify, Response, stream_with_context
10
+ from flask import Flask, request, send_file, jsonify
13
11
 
14
- from .downloader import validate_environment, download_video_stream
12
+ from .downloader import validate_environment, download_video
15
13
 
16
14
  app = Flask(__name__)
17
15
 
@@ -67,49 +65,8 @@ def _release_job_slot() -> None:
67
65
  _sem.release()
68
66
 
69
67
 
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
-
103
68
  @app.route("/api/download", methods=["POST"])
104
69
  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
- """
113
70
  _cleanup_stale_jobs()
114
71
 
115
72
  if not _try_acquire_job_slot():
@@ -143,88 +100,43 @@ def handle_download():
143
100
  ), 400
144
101
 
145
102
  job_dir = tempfile.mkdtemp(prefix="ytpdl_", dir=BASE_DOWNLOAD_DIR)
146
- job_id = os.path.basename(job_dir)
147
103
 
148
- def stream():
149
- nonlocal job_dir
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:
150
122
  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:
195
123
  if job_dir:
196
124
  shutil.rmtree(job_dir, ignore_errors=True)
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
125
+ except Exception:
126
+ pass
213
127
 
214
- except Exception as e:
215
- if job_dir:
216
- shutil.rmtree(job_dir, ignore_errors=True)
217
- _release_once()
128
+ response.call_on_close(_cleanup)
129
+ return response
218
130
 
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
131
+ except RuntimeError as e:
132
+ if job_dir:
133
+ shutil.rmtree(job_dir, ignore_errors=True)
134
+ _release_once()
223
135
 
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
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
228
140
 
229
141
  except Exception as e:
230
142
  if job_dir:
@@ -233,42 +145,6 @@ def handle_download():
233
145
  return jsonify(error=f"Download failed: {str(e)}"), 500
234
146
 
235
147
 
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
-
272
148
  @app.route("/healthz", methods=["GET"])
273
149
  def healthz():
274
150
  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, Generator
9
+ from typing import Optional, List, Tuple
10
10
 
11
11
  # =========================
12
12
  # Config / constants
@@ -60,6 +60,11 @@ 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
+
63
68
  # =========================
64
69
  # Environment / Mullvad
65
70
  # =========================
@@ -130,25 +135,85 @@ def _common_flags() -> List[str]:
130
135
  "--user-agent", MODERN_UA,
131
136
  "--no-cache-dir",
132
137
  "--ignore-config",
138
+ "--embed-metadata",
133
139
  "--sleep-interval", "1",
134
140
  ]
135
141
 
136
142
 
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
- )
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.
144
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)
145
155
 
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}]"
156
+ for raw in (stdout or "").splitlines():
157
+ line = (raw or "").strip()
158
+ if not line:
159
+ continue
149
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
150
165
 
151
- def _newest_non_temp_file(out_dir: str) -> Optional[str]:
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
+
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
210
+
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
+
216
+ # 4) Fallback: newest non-temp file in out_dir
152
217
  try:
153
218
  best_path = None
154
219
  best_mtime = -1.0
@@ -162,129 +227,80 @@ def _newest_non_temp_file(out_dir: str) -> Optional[str]:
162
227
  if mt > best_mtime:
163
228
  best_mtime = mt
164
229
  best_path = full
165
- return best_path
230
+ if best_path:
231
+ return best_path
166
232
  except Exception:
167
- return None
233
+ pass
234
+
235
+ return None
168
236
 
169
237
 
170
- def _download_with_format_stream(
171
- *,
238
+ def _download_with_format(
172
239
  url: str,
173
240
  out_dir: str,
174
241
  fmt: str,
175
242
  merge_output_format: Optional[str] = None,
176
243
  extract_mp3: bool = False,
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
- """
244
+ ) -> str:
184
245
  out_dir = os.path.abspath(out_dir)
185
246
  os.makedirs(out_dir, exist_ok=True)
186
247
 
187
248
  out_tpl = os.path.join(out_dir, "%(title)s.%(ext)s")
188
249
 
189
- argv: List[str] = [
250
+ argv = [
190
251
  YTDLP_BIN,
191
- url,
192
- "--progress",
193
- "--newline",
194
- "--continue",
195
252
  "-f", fmt,
196
253
  *(_common_flags()),
197
254
  "--output", out_tpl,
198
- # Absolute final path for internal capture (we do NOT emit this line).
255
+ # Ensure we can reliably pick the final output path.
199
256
  "--print", "after_move:filepath",
200
- # Local parity signal:
201
- "--print", "after_move:[download_complete] %(title)s.%(ext)s",
202
257
  ]
203
258
 
204
259
  if extract_mp3:
205
- argv.extend(
206
- [
207
- "--extract-audio",
208
- "--audio-format", "mp3",
209
- "--audio-quality", "0",
210
- "--embed-thumbnail",
211
- "--add-metadata",
212
- ]
213
- )
260
+ # Force audio extraction to MP3 (requires ffmpeg)
261
+ argv.extend(["--extract-audio", "--audio-format", "mp3"])
214
262
 
263
+ # Only force merge container when we actually want MP4 output.
215
264
  if merge_output_format:
216
265
  argv.extend(["--merge-output-format", merge_output_format])
217
266
 
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
- )
226
-
227
- final_path: Optional[str] = None
228
- tail_lines: List[str] = []
267
+ argv.append(url)
229
268
 
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
236
-
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
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)
269
+ rc, out = _run_argv_capture(argv)
270
+ path = _extract_final_path(out, out_dir)
246
271
 
247
- # Emit everything else (yt-dlp progress + [download_complete] line)
248
- yield line
272
+ if path and os.path.exists(path):
273
+ return os.path.abspath(path)
249
274
 
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)
275
+ tail = _tail(out)
276
+ if rc != 0:
261
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}")
262
279
 
263
- # Resolve final path
264
- if final_path and os.path.exists(final_path):
265
- return os.path.abspath(final_path)
266
280
 
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)
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
+ )
271
288
 
272
- tail = _tail("\n".join(tail_lines))
273
- raise RuntimeError(f"Download completed but output file not found (format: {fmt})\n{tail}")
289
+
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}]"
274
293
 
275
294
 
276
295
  # =========================
277
- # Public APIs
296
+ # Public API
278
297
  # =========================
279
- def download_video_stream(
298
+ def download_video(
280
299
  url: str,
281
300
  resolution: int | None = 1080,
282
301
  extension: Optional[str] = None,
283
302
  out_dir: str = DEFAULT_OUT_DIR,
284
- ) -> Generator[str, None, str]:
285
- """
286
- Streams yt-dlp logs and returns final file path (generator return value).
287
- """
303
+ ) -> str:
288
304
  if not url:
289
305
  raise RuntimeError("Missing URL")
290
306
 
@@ -302,68 +318,46 @@ def download_video_stream(
302
318
  mode = (extension or "mp4").lower().strip()
303
319
 
304
320
  if mode == "mp3":
305
- return (yield from _download_with_format_stream(
321
+ # bestaudio -> ffmpeg -> mp3 (post-processed by yt-dlp)
322
+ return _download_with_format(
306
323
  url=url,
307
324
  out_dir=out_dir,
308
325
  fmt="bestaudio",
309
326
  merge_output_format=None,
310
327
  extract_mp3=True,
311
- ))
328
+ )
312
329
 
313
330
  cap = int(resolution or 1080)
314
331
 
315
332
  if mode == "best":
316
- # Try best first (may produce webm/mkv/etc). If it fails, fall back to Apple-safe MP4.
333
+ # Try best first (may produce webm/mkv/etc).
317
334
  try:
318
- return (yield from _download_with_format_stream(
335
+ return _download_with_format(
319
336
  url=url,
320
337
  out_dir=out_dir,
321
338
  fmt=_fmt_best(cap),
322
339
  merge_output_format=None,
323
340
  extract_mp3=False,
324
- ))
341
+ )
325
342
  except Exception:
326
- return (yield from _download_with_format_stream(
343
+ # If best fails for any reason, fall back to Apple-safe MP4.
344
+ return _download_with_format(
327
345
  url=url,
328
346
  out_dir=out_dir,
329
347
  fmt=_fmt_mp4_apple_safe(cap),
330
348
  merge_output_format="mp4",
331
349
  extract_mp3=False,
332
- ))
350
+ )
333
351
 
334
- # Default / "mp4" mode
335
- return (yield from _download_with_format_stream(
352
+ # Default / "mp4" mode: force Apple-safe MP4 up to cap.
353
+ return _download_with_format(
336
354
  url=url,
337
355
  out_dir=out_dir,
338
356
  fmt=_fmt_mp4_apple_safe(cap),
339
357
  merge_output_format="mp4",
340
358
  extract_mp3=False,
341
- ))
359
+ )
342
360
 
343
361
  finally:
344
362
  if _mullvad_present():
345
363
  _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.8
3
+ Version: 0.6.9
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.8 yt-dlp[default]
60
+ pip install ytp-dl==0.6.9 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.8` + `yt-dlp[default]` + `gunicorn`
245
+ * Installs `ytp-dl==0.6.9` + `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.8 + yt-dlp[default] + gunicorn in that venv
261
+ # - Installs ytp-dl==0.6.9 + 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.8" "yt-dlp[default]" gunicorn
397
+ pip install "ytp-dl==0.6.9" "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=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,,
@@ -1,8 +0,0 @@
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,,
File without changes