easyder 0.2.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.
easyder/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .core import rz
2
+
3
+ __all__ = ["rz"]
easyder/core.py ADDED
@@ -0,0 +1,150 @@
1
+ import inspect
2
+
3
+ from .parser import parse, STAGES
4
+
5
+
6
+ COOKIE_VARIABLE = "Cooki"
7
+
8
+
9
+ class EasyDer:
10
+
11
+ def __init__(self, cookies=None):
12
+ import os
13
+ from .downloader import Downloader
14
+
15
+ self.downloader = Downloader()
16
+ self.cookies = cookies or []
17
+
18
+ def _get_working_cookie(self, url):
19
+ if not self.cookies:
20
+ return None
21
+
22
+ print("Checking cookies...")
23
+
24
+ for cookie in self.cookies:
25
+ if not cookie:
26
+ continue
27
+
28
+ if not self.downloader.cookie_exists(cookie):
29
+ print(f"āŒ Cookie file not found: {cookie}")
30
+ print(" → Continuing...")
31
+ continue
32
+
33
+ try:
34
+ print(f"šŸ” Testing cookie: {cookie}")
35
+
36
+ self.downloader.test_cookie(
37
+ url,
38
+ cookie
39
+ )
40
+
41
+ print(f"āœ… Cookie works: {cookie}")
42
+ return cookie
43
+
44
+ except Exception as e:
45
+ print(f"āŒ Cookie failed: {cookie}")
46
+ print(f" → {e}")
47
+ print(" → Continuing...")
48
+
49
+ print("āš ļø No working cookie found.")
50
+ return None
51
+
52
+ def execute(self, expression, url=None, progress_callback=None):
53
+ config = parse(expression)
54
+
55
+ if url is not None:
56
+ config["url"] = url
57
+
58
+ if not config["url"]:
59
+ raise ValueError("URL missing")
60
+
61
+ try:
62
+ return self.downloader.execute(
63
+ config,
64
+ cookies=None,
65
+ progress_callback=progress_callback
66
+ )
67
+
68
+ except Exception as normal_error:
69
+ print("\nāš ļø Normal request failed.")
70
+
71
+ working_cookie = self._get_working_cookie(
72
+ config["url"]
73
+ )
74
+
75
+ if working_cookie is None:
76
+ print("āŒ No working cookie available.")
77
+ raise normal_error
78
+
79
+ print(
80
+ f"šŸŖ Using working cookie: "
81
+ f"{working_cookie}"
82
+ )
83
+
84
+ return self.downloader.execute(
85
+ config,
86
+ cookies=working_cookie,
87
+ progress_callback=progress_callback
88
+ )
89
+
90
+ def __call__(
91
+ self,
92
+ expression,
93
+ url=None,
94
+ progress_callback=None
95
+ ):
96
+ return self.execute(
97
+ expression,
98
+ url,
99
+ progress_callback
100
+ )
101
+
102
+
103
+ def _get_user_cookies():
104
+ frame = inspect.currentframe()
105
+
106
+ try:
107
+ if frame is None:
108
+ return []
109
+
110
+ caller = frame.f_back
111
+
112
+ while caller is not None:
113
+ cookies = caller.f_locals.get(
114
+ COOKIE_VARIABLE
115
+ )
116
+
117
+ if cookies is None:
118
+ cookies = caller.f_globals.get(
119
+ COOKIE_VARIABLE
120
+ )
121
+
122
+ if cookies is not None:
123
+ if isinstance(cookies, str):
124
+ return [cookies]
125
+
126
+ if isinstance(cookies, (list, tuple)):
127
+ return list(cookies)
128
+
129
+ caller = caller.f_back
130
+
131
+ return []
132
+
133
+ finally:
134
+ del frame
135
+
136
+
137
+ def rz(
138
+ expression,
139
+ url=None,
140
+ progress_callback=None
141
+ ):
142
+ cookies = _get_user_cookies()
143
+
144
+ handler = EasyDer(cookies)
145
+
146
+ return handler.execute(
147
+ expression,
148
+ url,
149
+ progress_callback
150
+ )
easyder/downloader.py ADDED
@@ -0,0 +1,571 @@
1
+ import os
2
+ import urllib.request
3
+ import yt_dlp
4
+
5
+ from .parser import STAGES
6
+
7
+
8
+ class Downloader:
9
+
10
+ def __init__(self):
11
+ self.path = "/storage/emulated/0/Download/ReiDownloader"
12
+ os.makedirs(self.path, exist_ok=True)
13
+
14
+ # ============================================================
15
+ # Helpers
16
+ # ============================================================
17
+
18
+ def _format_speed(self, speed):
19
+ if not speed:
20
+ return "0 KB/s"
21
+
22
+ if speed >= 1024 * 1024:
23
+ return f"{speed / (1024 * 1024):.1f} MB/s"
24
+
25
+ return f"{speed / 1024:.0f} KB/s"
26
+
27
+ def _progress_hook(
28
+ self,
29
+ data,
30
+ mode="Video",
31
+ progress_callback=None
32
+ ):
33
+
34
+ status = data.get("status")
35
+
36
+ if status == "downloading":
37
+
38
+ total = (
39
+ data.get("total_bytes")
40
+ or data.get("total_bytes_estimate")
41
+ )
42
+
43
+ downloaded = data.get("downloaded_bytes", 0)
44
+ speed = data.get("speed")
45
+
46
+ if total:
47
+ percent = (downloaded / total) * 100
48
+ else:
49
+ percent = 0
50
+
51
+ network = self._format_speed(speed)
52
+
53
+ print(
54
+ f"\r[Download {percent:.0f}%] "
55
+ f"[Network {network}] "
56
+ f"[Mode {mode}]",
57
+ end="",
58
+ flush=True
59
+ )
60
+
61
+ if progress_callback:
62
+ try:
63
+ progress_callback(
64
+ f"{percent:.0f}%",
65
+ network,
66
+ mode
67
+ )
68
+ except Exception:
69
+ pass
70
+
71
+ elif status == "finished":
72
+
73
+ network = self._format_speed(
74
+ data.get("speed")
75
+ )
76
+
77
+ print(
78
+ f"\r[Download 100%] "
79
+ f"[Network {network}] "
80
+ f"[Mode {mode}]"
81
+ )
82
+
83
+ if progress_callback:
84
+ try:
85
+ progress_callback(
86
+ "100%",
87
+ network,
88
+ mode
89
+ )
90
+ except Exception:
91
+ pass
92
+
93
+ def _get_format(self, quality):
94
+
95
+ if quality == "audio":
96
+
97
+ return (
98
+ "bestaudio[acodec^=mp4a]/"
99
+ "bestaudio"
100
+ )
101
+
102
+ elif quality == "best":
103
+
104
+ return (
105
+ "bestvideo[vcodec^=avc1]+"
106
+ "bestaudio[acodec^=mp4a]/"
107
+ "bestvideo[vcodec^=avc1]+"
108
+ "bestaudio/"
109
+ "best"
110
+ )
111
+
112
+ else:
113
+
114
+ height = quality.replace("p", "")
115
+
116
+ return (
117
+ f"bestvideo[height<={height}]"
118
+ f"[vcodec^=avc1]+"
119
+ f"bestaudio[acodec^=mp4a]/"
120
+ f"bestvideo[height<={height}]"
121
+ f"[vcodec^=avc1]+"
122
+ f"bestaudio/"
123
+ f"best"
124
+ )
125
+
126
+ # ============================================================
127
+ # Main Engine
128
+ # ============================================================
129
+
130
+ def cookie_exists(self, cookie):
131
+ return os.path.isfile(cookie)
132
+
133
+ def test_cookie(self, url, cookie):
134
+ options = {
135
+ "quiet": True,
136
+ "no_warnings": True,
137
+ "cookiefile": cookie
138
+ }
139
+
140
+ with yt_dlp.YoutubeDL(options) as ydl:
141
+ ydl.extract_info(
142
+ url,
143
+ download=False
144
+ )
145
+
146
+ def execute(
147
+ self,
148
+ config,
149
+ cookies=None,
150
+ progress_callback=None
151
+ ):
152
+
153
+ url = config["url"]
154
+
155
+ # ---------------------------------
156
+ # Cookie validation
157
+ # ---------------------------------
158
+
159
+ if cookies:
160
+
161
+ if not os.path.isfile(cookies):
162
+ raise FileNotFoundError(
163
+ f"Cookies file not found: {cookies}"
164
+ )
165
+
166
+ # ---------------------------------
167
+ # Find playlist block
168
+ # ---------------------------------
169
+
170
+ playlist = None
171
+
172
+ for block in config["blocks"]:
173
+
174
+ if block["type"] == "playlist":
175
+ playlist = block
176
+ break
177
+
178
+ # ---------------------------------
179
+ # Find video block
180
+ # ---------------------------------
181
+
182
+ video = None
183
+
184
+ for block in config["blocks"]:
185
+
186
+ if block["type"] == "video":
187
+ video = block
188
+ break
189
+
190
+ # =========================================================
191
+ # PLAYLIST ENGINE
192
+ # =========================================================
193
+
194
+ if playlist is not None:
195
+
196
+ quality_stage = playlist["quality"]
197
+ skip = playlist["skip"]
198
+ count = playlist["count"]
199
+
200
+ quality = STAGES[quality_stage]
201
+
202
+ fmt = self._get_format(quality)
203
+
204
+ # -----------------------------------------------------
205
+ # Metadata + playlist title
206
+ # -----------------------------------------------------
207
+
208
+ metadata_options = {
209
+ "quiet": True,
210
+ "no_warnings": True,
211
+ "extract_flat": True,
212
+ "noplaylist": False
213
+ }
214
+
215
+ if cookies:
216
+ metadata_options["cookiefile"] = cookies
217
+
218
+ with yt_dlp.YoutubeDL(
219
+ metadata_options
220
+ ) as ydl:
221
+
222
+ playlist_info = ydl.extract_info(
223
+ url,
224
+ download=False
225
+ )
226
+
227
+ result = {}
228
+
229
+ # -----------------------------------------------------
230
+ # Metadata blocks
231
+ # -----------------------------------------------------
232
+
233
+ for block in config["blocks"]:
234
+
235
+ variable = block["variable"]
236
+
237
+ if not variable:
238
+ continue
239
+
240
+ block_type = block["type"]
241
+
242
+ if block_type == "title":
243
+
244
+ result[variable] = playlist_info.get(
245
+ "title"
246
+ )
247
+
248
+ elif block_type == "creator":
249
+
250
+ result[variable] = (
251
+ playlist_info.get("uploader")
252
+ or playlist_info.get("channel")
253
+ )
254
+
255
+ elif block_type == "url":
256
+
257
+ result[variable] = (
258
+ playlist_info.get("webpage_url")
259
+ or url
260
+ )
261
+
262
+ elif block_type == "views":
263
+
264
+ result[variable] = playlist_info.get(
265
+ "view_count"
266
+ )
267
+
268
+ elif block_type == "likes":
269
+
270
+ result[variable] = playlist_info.get(
271
+ "like_count"
272
+ )
273
+
274
+ elif block_type == "count":
275
+
276
+ result[variable] = playlist_info.get(
277
+ "comment_count"
278
+ )
279
+
280
+ elif block_type == "time":
281
+
282
+ result[variable] = playlist_info.get(
283
+ "duration"
284
+ )
285
+
286
+ elif block_type == "playlist_title":
287
+
288
+ result[variable] = playlist_info.get(
289
+ "title"
290
+ )
291
+
292
+ # -----------------------------------------------------
293
+ # Playlist range
294
+ # -----------------------------------------------------
295
+
296
+ start = skip + 1
297
+ end = skip + count
298
+
299
+ playlist_items = f"{start}-{end}"
300
+
301
+ output = os.path.join(
302
+ self.path,
303
+ "%(title)s.%(ext)s"
304
+ )
305
+
306
+ options = {
307
+ "format": fmt,
308
+ "outtmpl": output,
309
+ "noplaylist": False,
310
+ "playlist_items": playlist_items,
311
+ "merge_output_format": "mp4",
312
+ "quiet": True,
313
+ "no_warnings": True,
314
+ "progress_hooks": [
315
+ lambda data: self._progress_hook(
316
+ data,
317
+ "Playlist",
318
+ progress_callback
319
+ )
320
+ ]
321
+ }
322
+
323
+ if cookies:
324
+ options["cookiefile"] = cookies
325
+
326
+ print(
327
+ f"Playlist download: "
328
+ f"quality={quality}, "
329
+ f"skip={skip}, "
330
+ f"count={count}"
331
+ )
332
+
333
+ # -----------------------------------------------------
334
+ # Download playlist
335
+ # -----------------------------------------------------
336
+
337
+ downloaded_files = []
338
+
339
+ with yt_dlp.YoutubeDL(options) as ydl:
340
+
341
+ final_info = ydl.extract_info(
342
+ url,
343
+ download=True
344
+ )
345
+
346
+ entries = final_info.get(
347
+ "entries",
348
+ []
349
+ )
350
+
351
+ for entry in entries:
352
+
353
+ if not entry:
354
+ continue
355
+
356
+ filepath = ydl.prepare_filename(
357
+ entry
358
+ )
359
+
360
+ if not os.path.exists(filepath):
361
+
362
+ mp4_path = (
363
+ os.path.splitext(filepath)[0]
364
+ + ".mp4"
365
+ )
366
+
367
+ if os.path.exists(mp4_path):
368
+ filepath = mp4_path
369
+
370
+ if os.path.exists(filepath):
371
+
372
+ downloaded_files.append(
373
+ filepath
374
+ )
375
+
376
+ # -----------------------------------------------------
377
+ # Return playlist files
378
+ # -----------------------------------------------------
379
+
380
+ result["videos"] = downloaded_files
381
+
382
+ return result
383
+
384
+ # =========================================================
385
+ # NORMAL SINGLE-VIDEO ENGINE
386
+ # =========================================================
387
+
388
+ metadata_options = {
389
+ "quiet": True,
390
+ "no_warnings": True
391
+ }
392
+
393
+ if cookies:
394
+ metadata_options["cookiefile"] = cookies
395
+
396
+ with yt_dlp.YoutubeDL(
397
+ metadata_options
398
+ ) as ydl:
399
+
400
+ info = ydl.extract_info(
401
+ url,
402
+ download=False
403
+ )
404
+
405
+ result = {}
406
+
407
+ # ---------------------------------
408
+ # Metadata blocks
409
+ # ---------------------------------
410
+
411
+ for block in config["blocks"]:
412
+
413
+ variable = block["variable"]
414
+
415
+ if not variable:
416
+ continue
417
+
418
+ block_type = block["type"]
419
+
420
+ if block_type == "title":
421
+
422
+ result[variable] = info.get(
423
+ "title"
424
+ )
425
+
426
+ elif block_type == "creator":
427
+
428
+ result[variable] = (
429
+ info.get("uploader")
430
+ or info.get("channel")
431
+ )
432
+
433
+ elif block_type == "url":
434
+
435
+ result[variable] = (
436
+ info.get("webpage_url")
437
+ or url
438
+ )
439
+
440
+ elif block_type == "views":
441
+
442
+ result[variable] = info.get(
443
+ "view_count"
444
+ )
445
+
446
+ elif block_type == "likes":
447
+
448
+ result[variable] = info.get(
449
+ "like_count"
450
+ )
451
+
452
+ elif block_type == "count":
453
+
454
+ result[variable] = info.get(
455
+ "comment_count"
456
+ )
457
+
458
+ elif block_type == "time":
459
+
460
+ result[variable] = info.get(
461
+ "duration"
462
+ )
463
+
464
+ elif block_type == "thumbnail":
465
+
466
+ thumbnail_url = info.get(
467
+ "thumbnail"
468
+ )
469
+
470
+ if thumbnail_url:
471
+
472
+ title = (
473
+ info.get("title")
474
+ or "thumbnail"
475
+ )
476
+
477
+ thumbnail_path = os.path.join(
478
+ self.path,
479
+ f"{title}.jpg"
480
+ )
481
+
482
+ urllib.request.urlretrieve(
483
+ thumbnail_url,
484
+ thumbnail_path
485
+ )
486
+
487
+ result[variable] = thumbnail_path
488
+
489
+ else:
490
+
491
+ result[variable] = None
492
+
493
+ elif block_type == "formats":
494
+
495
+ result[variable] = info.get(
496
+ "formats",
497
+ []
498
+ )
499
+
500
+ # ---------------------------------
501
+ # Metadata-only request
502
+ # ---------------------------------
503
+
504
+ if video is None:
505
+ return result
506
+
507
+ # ---------------------------------
508
+ # Quality
509
+ # ---------------------------------
510
+
511
+ stage = video["stage"]
512
+
513
+ quality = STAGES[stage]
514
+
515
+ fmt = self._get_format(quality)
516
+
517
+ # ---------------------------------
518
+ # Download options
519
+ # ---------------------------------
520
+
521
+ output = os.path.join(
522
+ self.path,
523
+ "%(title)s.%(ext)s"
524
+ )
525
+
526
+ options = {
527
+ "format": fmt,
528
+ "outtmpl": output,
529
+ "noplaylist": True,
530
+ "merge_output_format": "mp4",
531
+ "quiet": True,
532
+ "progress_hooks": [
533
+ lambda data: self._progress_hook(
534
+ data,
535
+ "Video",
536
+ progress_callback
537
+ )
538
+ ]
539
+ }
540
+
541
+ if cookies:
542
+ options["cookiefile"] = cookies
543
+
544
+ print(
545
+ f"Downloading: {quality}"
546
+ )
547
+
548
+ with yt_dlp.YoutubeDL(options) as ydl:
549
+
550
+ final_info = ydl.extract_info(
551
+ url,
552
+ download=True
553
+ )
554
+
555
+ filepath = ydl.prepare_filename(
556
+ final_info
557
+ )
558
+
559
+ if not os.path.exists(filepath):
560
+
561
+ mp4_path = (
562
+ os.path.splitext(filepath)[0]
563
+ + ".mp4"
564
+ )
565
+
566
+ if os.path.exists(mp4_path):
567
+ filepath = mp4_path
568
+
569
+ result[video["variable"]] = filepath
570
+
571
+ return result
easyder/parser.py ADDED
@@ -0,0 +1,88 @@
1
+ STAGES = {
2
+ 0: "audio",
3
+ 1: "360p",
4
+ 2: "480p",
5
+ 3: "720p",
6
+ 4: "1080p",
7
+ 5: "best",
8
+ }
9
+
10
+
11
+ def parse(expression):
12
+ if not isinstance(expression, dict):
13
+ raise ValueError("Easyder syntax must be a dictionary")
14
+
15
+ result = {
16
+ "url": None,
17
+ "blocks": []
18
+ }
19
+
20
+ for key, variable in expression.items():
21
+ key = str(key).strip()
22
+ variable = str(variable).strip()
23
+
24
+ if key.startswith("video[") and key.endswith("]"):
25
+ stage_text = key[6:-1]
26
+
27
+ try:
28
+ stage = int(stage_text)
29
+ except ValueError:
30
+ raise ValueError(f"Invalid video stage: {stage_text}")
31
+
32
+ if stage not in STAGES:
33
+ raise ValueError(f"Unknown video stage: {stage}")
34
+
35
+ result["blocks"].append({
36
+ "type": "video",
37
+ "stage": stage,
38
+ "variable": variable
39
+ })
40
+ continue
41
+
42
+ if key == "playlist":
43
+ parts = variable.split(":")
44
+
45
+ if len(parts) != 3:
46
+ raise ValueError(
47
+ "Playlist syntax: QUALITY:SKIP:COUNT"
48
+ )
49
+
50
+ try:
51
+ quality = int(parts[0])
52
+ skip = int(parts[1])
53
+ count = int(parts[2])
54
+ except ValueError:
55
+ raise ValueError(
56
+ "Playlist values must be numbers"
57
+ )
58
+
59
+ if quality not in STAGES:
60
+ raise ValueError(
61
+ f"Unknown playlist quality: {quality}"
62
+ )
63
+
64
+ if skip < 0:
65
+ raise ValueError(
66
+ "Playlist skip cannot be negative"
67
+ )
68
+
69
+ if count <= 0:
70
+ raise ValueError(
71
+ "Playlist count must be greater than 0"
72
+ )
73
+
74
+ result["blocks"].append({
75
+ "type": "playlist",
76
+ "quality": quality,
77
+ "skip": skip,
78
+ "count": count,
79
+ "variable": None
80
+ })
81
+ continue
82
+
83
+ result["blocks"].append({
84
+ "type": key,
85
+ "variable": variable
86
+ })
87
+
88
+ return result
@@ -0,0 +1,504 @@
1
+ Metadata-Version: 2.4
2
+ Name: easyder
3
+ Version: 0.2.0
4
+ Summary: EASYDER - CSS-inspired video downloader with automatic cookie fallback, powered by yt-dlp + FFmpeg
5
+ Home-page: https://github.com/ReiZyuki/easyder
6
+ Author: ReiZyuki
7
+ Author-email: ReiZyuki <rei@example.com>
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/ReiZyuki/easyder
10
+ Project-URL: Bug Tracker, https://github.com/ReiZyuki/easyder/issues
11
+ Project-URL: Documentation, https://github.com/ReiZyuki/easyder#readme
12
+ Project-URL: Source Code, https://github.com/ReiZyuki/easyder
13
+ Keywords: video,downloader,youtube,instagram,tiktok,yt-dlp,ffmpeg,css-inspired,easy
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: End Users/Desktop
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Natural Language :: English
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.8
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Topic :: Internet :: WWW/HTTP
27
+ Classifier: Topic :: Multimedia :: Video
28
+ Classifier: Topic :: Utilities
29
+ Requires-Python: >=3.8
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Requires-Dist: yt-dlp>=2023.12.30
33
+ Requires-Dist: requests>=2.31.0
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
36
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
37
+ Requires-Dist: black>=23.0.0; extra == "dev"
38
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
39
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
40
+ Dynamic: author
41
+ Dynamic: home-page
42
+ Dynamic: license-file
43
+ Dynamic: requires-python
44
+
45
+ EASYDER — CSS-Inspired Video Downloader
46
+
47
+ ""Python 3.8+" (https://img.shields.io/badge/python-3.8+-blue.svg)" (https://www.python.org/downloads/)
48
+ ""License: MIT" (https://img.shields.io/badge/License-MIT-yellow.svg)" (https://opensource.org/licenses/MIT)
49
+
50
+ EASYDER is a CSS-inspired video downloader built with yt-dlp + FFmpeg.
51
+
52
+ Easyder V1.0 focuses on downloading video/audio, extracting metadata, downloading thumbnails, handling playlists, and automatically trying supplied cookie files when a normal request fails.
53
+
54
+ ---
55
+
56
+ Features
57
+
58
+ - āœ… CSS-inspired dictionary syntax
59
+ - āœ… Video quality selection
60
+ - āœ… Audio-only downloads
61
+ - āœ… 360p, 480p, 720p and 1080p quality stages
62
+ - āœ… Original/best quality selection
63
+ - āœ… Automatic cookie fallback
64
+ - āœ… Multiple supplied cookie files
65
+ - āœ… Playlist downloads
66
+ - āœ… Video metadata extraction
67
+ - āœ… Thumbnail downloading
68
+ - āœ… yt-dlp + FFmpeg based downloading
69
+ - āœ… Download progress display
70
+ - āœ… MP4 output for video downloads
71
+
72
+ ---
73
+
74
+ Installation
75
+
76
+ From PyPI
77
+
78
+ pip install easyder
79
+
80
+ From GitHub
81
+
82
+ git clone https://github.com/ReiZyuki/Easyder.git
83
+ cd Easyder
84
+ pip install -e .
85
+
86
+ ---
87
+
88
+ Quick Start
89
+
90
+ from easyder import rz
91
+
92
+ result = rz(
93
+ {
94
+ "video[3]": "video",
95
+ "title": "title",
96
+ "creator": "creator",
97
+ },
98
+ url,
99
+ )
100
+
101
+ print("Video:", result["video"])
102
+ print("Title:", result["title"])
103
+ print("Creator:", result["creator"])
104
+
105
+ The right-side values such as ""video"", ""title"" and ""creator"" are user-defined variable names.
106
+
107
+ ---
108
+
109
+ Video Quality
110
+
111
+ Easyder uses CSS-inspired quality selectors:
112
+
113
+ "video[0]"
114
+ "video[1]"
115
+ "video[2]"
116
+ "video[3]"
117
+ "video[4]"
118
+ "video[5]"
119
+
120
+ Selector| Quality
121
+ "video[0]"| Audio only
122
+ "video[1]"| 360p
123
+ "video[2]"| 480p
124
+ "video[3]"| 720p
125
+ "video[4]"| 1080p
126
+ "video[5]"| Best/original available quality
127
+
128
+ Example:
129
+
130
+ from easyder import rz
131
+
132
+ result = rz(
133
+ {
134
+ "video[4]": "video",
135
+ },
136
+ url,
137
+ )
138
+
139
+ print(result["video"])
140
+
141
+ If the requested quality is unavailable, yt-dlp selects an appropriate available format according to Easyder's format selection rules.
142
+
143
+ ---
144
+
145
+ Audio Only
146
+
147
+ Use "video[0]" for audio:
148
+
149
+ from easyder import rz
150
+
151
+ result = rz(
152
+ {
153
+ "video[0]": "audio",
154
+ },
155
+ url,
156
+ )
157
+
158
+ print(result["audio"])
159
+
160
+ ---
161
+
162
+ Metadata
163
+
164
+ Easyder can extract video metadata without downloading the video.
165
+
166
+ from easyder import rz
167
+
168
+ result = rz(
169
+ {
170
+ "title": "title",
171
+ "creator": "creator",
172
+ "url": "video_url",
173
+ "views": "views",
174
+ "likes": "likes",
175
+ "count": "comments",
176
+ "time": "duration",
177
+ "thumbnail": "thumbnail",
178
+ "formats": "formats",
179
+ },
180
+ url,
181
+ )
182
+
183
+ print("Title:", result["title"])
184
+ print("Creator:", result["creator"])
185
+ print("URL:", result["video_url"])
186
+ print("Views:", result["views"])
187
+ print("Likes:", result["likes"])
188
+ print("Comments:", result["comments"])
189
+ print("Duration:", result["duration"])
190
+ print("Thumbnail:", result["thumbnail"])
191
+ print("Formats:", result["formats"])
192
+
193
+ Metadata variable names are completely user-defined.
194
+
195
+ For example:
196
+
197
+ result = rz(
198
+ {
199
+ "title": "MyTitle",
200
+ "creator": "MyCreator",
201
+ },
202
+ url,
203
+ )
204
+
205
+ print(result["MyTitle"])
206
+ print(result["MyCreator"])
207
+
208
+ ---
209
+
210
+ Thumbnail
211
+
212
+ Use the "thumbnail" selector to download the video's thumbnail.
213
+
214
+ from easyder import rz
215
+
216
+ result = rz(
217
+ {
218
+ "thumbnail": "thumb",
219
+ },
220
+ url,
221
+ )
222
+
223
+ print(result["thumb"])
224
+
225
+ The returned value is the path of the downloaded thumbnail.
226
+
227
+ Default download directory:
228
+
229
+ /storage/emulated/0/Download/ReiDownloader
230
+
231
+ ---
232
+
233
+ Playlist
234
+
235
+ Playlist syntax:
236
+
237
+ "playlist": "QUALITY:SKIP:COUNT"
238
+
239
+ Example:
240
+
241
+ from easyder import rz
242
+
243
+ result = rz(
244
+ {
245
+ "playlist": "3:5:10",
246
+ },
247
+ playlist_url,
248
+ )
249
+
250
+ print(result["videos"])
251
+
252
+ Meaning:
253
+
254
+ 3 = 720p
255
+ 5 = skip the first 5 videos
256
+ 10 = download the next 10 videos
257
+
258
+ Quality values
259
+
260
+ Quality| Meaning
261
+ "0"| Audio
262
+ "1"| 360p
263
+ "2"| 480p
264
+ "3"| 720p
265
+ "4"| 1080p
266
+ "5"| Best/original
267
+
268
+ Example:
269
+
270
+ result = rz(
271
+ {
272
+ "playlist": "4:0:5",
273
+ },
274
+ playlist_url,
275
+ )
276
+
277
+ This requests:
278
+
279
+ 1080p
280
+ Skip 0
281
+ Download 5 videos
282
+
283
+ Downloaded playlist files are returned in:
284
+
285
+ result["videos"]
286
+
287
+ ---
288
+
289
+ Automatic Cookie Fallback
290
+
291
+ Easyder first attempts the request without cookies.
292
+
293
+ If the normal request fails, Easyder checks the cookie files supplied through the "Cooki" variable.
294
+
295
+ from easyder import rz
296
+
297
+ Cooki = [
298
+ "/storage/emulated/0/Download/youtube.txt",
299
+ "/storage/emulated/0/Download/other.txt",
300
+ ]
301
+
302
+ result = rz(
303
+ {
304
+ "video[3]": "video",
305
+ "title": "title",
306
+ },
307
+ url,
308
+ )
309
+
310
+ Easyder tests the supplied cookie files one by one and uses the first working cookie.
311
+
312
+ No cookie filenames are hardcoded by Easyder.
313
+
314
+ ---
315
+
316
+ Cookie Variable
317
+
318
+ The cookie variable name is:
319
+
320
+ Cooki
321
+
322
+ It can contain a single path:
323
+
324
+ Cooki = "/path/to/cookies.txt"
325
+
326
+ or multiple paths:
327
+
328
+ Cooki = [
329
+ "/path/to/cookies1.txt",
330
+ "/path/to/cookies2.txt",
331
+ ]
332
+
333
+ Easyder does not scan directories for cookie files.
334
+
335
+ ---
336
+
337
+ Progress
338
+
339
+ During downloads Easyder displays progress information similar to:
340
+
341
+ Downloading: 1080p
342
+ [Download 50%] [Network 2.0 MB/s] [Mode Video]
343
+
344
+ The downloader can also receive a progress callback:
345
+
346
+ from easyder import rz
347
+
348
+ def progress(percent, network, mode):
349
+ print(percent, network, mode)
350
+
351
+ result = rz(
352
+ {
353
+ "video[3]": "video",
354
+ },
355
+ url,
356
+ progress_callback=progress,
357
+ )
358
+
359
+ The callback receives:
360
+
361
+ percent
362
+ network
363
+ mode
364
+
365
+ ---
366
+
367
+ Custom Variable Names
368
+
369
+ Easyder does not require fixed output variable names.
370
+
371
+ For example:
372
+
373
+ result = rz(
374
+ {
375
+ "video[3]": "MyVideo",
376
+ "title": "MyTitle",
377
+ "creator": "MyCreator",
378
+ "thumbnail": "MyThumbnail",
379
+ },
380
+ url,
381
+ )
382
+
383
+ Then:
384
+
385
+ print(result["MyVideo"])
386
+ print(result["MyTitle"])
387
+ print(result["MyCreator"])
388
+ print(result["MyThumbnail"])
389
+
390
+ This keeps the selector syntax separate from the variable names used by your application.
391
+
392
+ ---
393
+
394
+ Complete Example
395
+
396
+ from easyder import rz
397
+
398
+ Cooki = [
399
+ "/storage/emulated/0/Download/youtube.txt",
400
+ ]
401
+
402
+ result = rz(
403
+ {
404
+ "video[4]": "video",
405
+ "title": "title",
406
+ "creator": "creator",
407
+ "thumbnail": "thumbnail",
408
+ "views": "views",
409
+ "likes": "likes",
410
+ "time": "duration",
411
+ },
412
+ url,
413
+ )
414
+
415
+ print("Video:", result["video"])
416
+ print("Title:", result["title"])
417
+ print("Creator:", result["creator"])
418
+ print("Thumbnail:", result["thumbnail"])
419
+ print("Views:", result["views"])
420
+ print("Likes:", result["likes"])
421
+ print("Duration:", result["duration"])
422
+
423
+ ---
424
+
425
+ Selector Reference
426
+
427
+ Selector| Example value| Description
428
+ "video[0]"| ""video""| Audio only
429
+ "video[1]"| ""video""| 360p video
430
+ "video[2]"| ""video""| 480p video
431
+ "video[3]"| ""video""| 720p video
432
+ "video[4]"| ""video""| 1080p video
433
+ "video[5]"| ""video""| Best/original quality
434
+ "title"| ""title""| Video title
435
+ "creator"| ""creator""| Uploader/creator
436
+ "url"| ""video_url""| Video URL
437
+ "views"| ""views""| View count
438
+ "likes"| ""likes""| Like count
439
+ "count"| ""comments""| Comment count
440
+ "time"| ""duration""| Duration
441
+ "thumbnail"| ""thumbnail""| Download thumbnail
442
+ "formats"| ""formats""| Available formats
443
+ "playlist"| ""QUALITY:SKIP:COUNT""| Playlist download
444
+
445
+ ---
446
+
447
+ Quality Reference
448
+
449
+ video[0] = Audio only
450
+ video[1] = 360p
451
+ video[2] = 480p
452
+ video[3] = 720p
453
+ video[4] = 1080p
454
+ video[5] = Best/original available
455
+
456
+ ---
457
+
458
+ Requirements
459
+
460
+ Easyder uses:
461
+
462
+ - Python
463
+ - yt-dlp
464
+ - FFmpeg
465
+
466
+ FFmpeg is required for media processing and merging separate video/audio streams.
467
+
468
+ ---
469
+
470
+ Download Location
471
+
472
+ The default download directory is:
473
+
474
+ /storage/emulated/0/Download/ReiDownloader
475
+
476
+ Videos and thumbnails downloaded by Easyder are stored there.
477
+
478
+ ---
479
+
480
+ V1.0 Scope
481
+
482
+ Easyder V1.0 intentionally focuses on:
483
+
484
+ Video
485
+ Audio
486
+ Thumbnail
487
+ Metadata
488
+ Playlist
489
+ Automatic cookie fallback
490
+ Progress reporting
491
+
492
+ Easyder V1.0 does not include:
493
+
494
+ Gallery / gallery-dl
495
+ Telegram / TeleBot integration
496
+ Sender functionality
497
+
498
+ These features may be considered separately in future versions.
499
+
500
+ ---
501
+
502
+ License
503
+
504
+ MIT License
@@ -0,0 +1,9 @@
1
+ easyder/__init__.py,sha256=lKyzNVVsHnac9yBcmb0vxwYtgHuX6UVNhaTJ_IllpTI,39
2
+ easyder/core.py,sha256=f6gOUDsmi5zWalOmxslS6FkKxgXTCdJwrILHIWLbY-w,3404
3
+ easyder/downloader.py,sha256=48LhUBhhqJXGE4-Mc_Sf61iiqcQghOe3rOq4SyCnyfA,14436
4
+ easyder/parser.py,sha256=7e3ionF0ibBcMzgGt93Ug2yLBKQTtQDVZvL6EmQKRmk,2224
5
+ easyder-0.2.0.dist-info/licenses/LICENSE,sha256=vPfguhmt6ouLqAZ0kAiP4zO2v01yvJ4477X24eh5Vvs,1065
6
+ easyder-0.2.0.dist-info/METADATA,sha256=9_zSpxN84LoDRK6aen9XZ7hT1KA1Tq87ZHgFSM-YMqU,9633
7
+ easyder-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ easyder-0.2.0.dist-info/top_level.txt,sha256=QGXPSOhE6ebeLjg_lLO6RXdG6OSc91s9Z9t8bgjFCaw,8
9
+ easyder-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ReiZyuki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ easyder