webscout 6.3__py3-none-any.whl → 6.5__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.

Potentially problematic release.


This version of webscout might be problematic. Click here for more details.

Files changed (131) hide show
  1. webscout/AIauto.py +191 -176
  2. webscout/AIbase.py +0 -197
  3. webscout/AIutel.py +441 -1130
  4. webscout/DWEBS.py +189 -35
  5. webscout/{YTdownloader.py → Extra/YTToolkit/YTdownloader.py} +990 -1103
  6. webscout/Extra/YTToolkit/__init__.py +3 -0
  7. webscout/{transcriber.py → Extra/YTToolkit/transcriber.py} +479 -551
  8. webscout/Extra/YTToolkit/ytapi/__init__.py +6 -0
  9. webscout/Extra/YTToolkit/ytapi/channel.py +307 -0
  10. webscout/Extra/YTToolkit/ytapi/errors.py +13 -0
  11. webscout/Extra/YTToolkit/ytapi/extras.py +45 -0
  12. webscout/Extra/YTToolkit/ytapi/https.py +88 -0
  13. webscout/Extra/YTToolkit/ytapi/patterns.py +61 -0
  14. webscout/Extra/YTToolkit/ytapi/playlist.py +59 -0
  15. webscout/Extra/YTToolkit/ytapi/pool.py +8 -0
  16. webscout/Extra/YTToolkit/ytapi/query.py +37 -0
  17. webscout/Extra/YTToolkit/ytapi/stream.py +60 -0
  18. webscout/Extra/YTToolkit/ytapi/utils.py +62 -0
  19. webscout/Extra/YTToolkit/ytapi/video.py +102 -0
  20. webscout/Extra/__init__.py +3 -1
  21. webscout/Extra/autocoder/__init__.py +9 -0
  22. webscout/Extra/autocoder/autocoder_utiles.py +121 -0
  23. webscout/Extra/autocoder/rawdog.py +680 -0
  24. webscout/Extra/autollama.py +246 -195
  25. webscout/Extra/gguf.py +81 -56
  26. webscout/Extra/markdownlite/__init__.py +862 -0
  27. webscout/Extra/weather_ascii.py +2 -2
  28. webscout/LLM.py +206 -43
  29. webscout/Litlogger/__init__.py +681 -0
  30. webscout/Provider/DARKAI.py +1 -1
  31. webscout/Provider/EDITEE.py +1 -1
  32. webscout/Provider/NinjaChat.py +1 -1
  33. webscout/Provider/PI.py +120 -35
  34. webscout/Provider/Perplexity.py +590 -598
  35. webscout/Provider/Reka.py +0 -1
  36. webscout/Provider/RoboCoders.py +206 -0
  37. webscout/Provider/TTI/AiForce/__init__.py +22 -0
  38. webscout/Provider/TTI/AiForce/async_aiforce.py +257 -0
  39. webscout/Provider/TTI/AiForce/sync_aiforce.py +242 -0
  40. webscout/Provider/TTI/Nexra/__init__.py +22 -0
  41. webscout/Provider/TTI/Nexra/async_nexra.py +286 -0
  42. webscout/Provider/TTI/Nexra/sync_nexra.py +258 -0
  43. webscout/Provider/TTI/PollinationsAI/__init__.py +23 -0
  44. webscout/Provider/TTI/PollinationsAI/async_pollinations.py +330 -0
  45. webscout/Provider/TTI/PollinationsAI/sync_pollinations.py +285 -0
  46. webscout/Provider/TTI/__init__.py +2 -4
  47. webscout/Provider/TTI/artbit/__init__.py +22 -0
  48. webscout/Provider/TTI/artbit/async_artbit.py +184 -0
  49. webscout/Provider/TTI/artbit/sync_artbit.py +176 -0
  50. webscout/Provider/TTI/blackbox/__init__.py +4 -0
  51. webscout/Provider/TTI/blackbox/async_blackbox.py +212 -0
  52. webscout/Provider/TTI/{blackboximage.py → blackbox/sync_blackbox.py} +199 -153
  53. webscout/Provider/TTI/deepinfra/__init__.py +4 -0
  54. webscout/Provider/TTI/deepinfra/async_deepinfra.py +227 -0
  55. webscout/Provider/TTI/deepinfra/sync_deepinfra.py +199 -0
  56. webscout/Provider/TTI/huggingface/__init__.py +22 -0
  57. webscout/Provider/TTI/huggingface/async_huggingface.py +199 -0
  58. webscout/Provider/TTI/huggingface/sync_huggingface.py +195 -0
  59. webscout/Provider/TTI/imgninza/__init__.py +4 -0
  60. webscout/Provider/TTI/imgninza/async_ninza.py +214 -0
  61. webscout/Provider/TTI/{imgninza.py → imgninza/sync_ninza.py} +209 -136
  62. webscout/Provider/TTI/talkai/__init__.py +4 -0
  63. webscout/Provider/TTI/talkai/async_talkai.py +229 -0
  64. webscout/Provider/TTI/talkai/sync_talkai.py +207 -0
  65. webscout/Provider/TTS/__init__.py +5 -1
  66. webscout/Provider/TTS/deepgram.py +183 -0
  67. webscout/Provider/TTS/elevenlabs.py +137 -0
  68. webscout/Provider/TTS/gesserit.py +151 -0
  69. webscout/Provider/TTS/murfai.py +139 -0
  70. webscout/Provider/TTS/parler.py +134 -107
  71. webscout/Provider/TTS/streamElements.py +360 -275
  72. webscout/Provider/TTS/utils.py +280 -0
  73. webscout/Provider/TTS/voicepod.py +116 -116
  74. webscout/Provider/__init__.py +8 -1
  75. webscout/Provider/askmyai.py +2 -2
  76. webscout/Provider/cerebras.py +227 -219
  77. webscout/Provider/llama3mitril.py +0 -1
  78. webscout/Provider/meta.py +794 -779
  79. webscout/Provider/mhystical.py +176 -0
  80. webscout/Provider/perplexitylabs.py +265 -0
  81. webscout/Provider/twitterclone.py +251 -245
  82. webscout/Provider/typegpt.py +358 -0
  83. webscout/__init__.py +9 -8
  84. webscout/__main__.py +5 -5
  85. webscout/cli.py +252 -280
  86. webscout/conversation.py +227 -0
  87. webscout/exceptions.py +161 -29
  88. webscout/litagent/__init__.py +172 -0
  89. webscout/litprinter/__init__.py +832 -0
  90. webscout/optimizers.py +270 -0
  91. webscout/prompt_manager.py +279 -0
  92. webscout/scout/__init__.py +11 -0
  93. webscout/scout/core.py +884 -0
  94. webscout/scout/element.py +459 -0
  95. webscout/scout/parsers/__init__.py +69 -0
  96. webscout/scout/parsers/html5lib_parser.py +172 -0
  97. webscout/scout/parsers/html_parser.py +236 -0
  98. webscout/scout/parsers/lxml_parser.py +178 -0
  99. webscout/scout/utils.py +38 -0
  100. webscout/swiftcli/__init__.py +810 -0
  101. webscout/update_checker.py +125 -0
  102. webscout/version.py +1 -1
  103. webscout/zeroart/__init__.py +55 -0
  104. webscout/zeroart/base.py +61 -0
  105. webscout/zeroart/effects.py +99 -0
  106. webscout/zeroart/fonts.py +816 -0
  107. webscout/zerodir/__init__.py +225 -0
  108. {webscout-6.3.dist-info → webscout-6.5.dist-info}/METADATA +37 -112
  109. webscout-6.5.dist-info/RECORD +179 -0
  110. webscout/Agents/Onlinesearcher.py +0 -182
  111. webscout/Agents/__init__.py +0 -2
  112. webscout/Agents/functioncall.py +0 -248
  113. webscout/Bing_search.py +0 -154
  114. webscout/Provider/TTI/AIuncensoredimage.py +0 -103
  115. webscout/Provider/TTI/Nexra.py +0 -120
  116. webscout/Provider/TTI/PollinationsAI.py +0 -138
  117. webscout/Provider/TTI/WebSimAI.py +0 -142
  118. webscout/Provider/TTI/aiforce.py +0 -160
  119. webscout/Provider/TTI/artbit.py +0 -141
  120. webscout/Provider/TTI/deepinfra.py +0 -148
  121. webscout/Provider/TTI/huggingface.py +0 -155
  122. webscout/Provider/TTI/talkai.py +0 -116
  123. webscout/g4f.py +0 -666
  124. webscout/models.py +0 -23
  125. webscout/requestsHTMLfix.py +0 -775
  126. webscout/webai.py +0 -2590
  127. webscout-6.3.dist-info/RECORD +0 -124
  128. {webscout-6.3.dist-info → webscout-6.5.dist-info}/LICENSE.md +0 -0
  129. {webscout-6.3.dist-info → webscout-6.5.dist-info}/WHEEL +0 -0
  130. {webscout-6.3.dist-info → webscout-6.5.dist-info}/entry_points.txt +0 -0
  131. {webscout-6.3.dist-info → webscout-6.5.dist-info}/top_level.txt +0 -0
@@ -1,1104 +1,991 @@
1
- import argparse
2
- from datetime import datetime
3
- import json
4
- import logging
5
- from time import sleep
6
- import requests
7
- from tqdm import tqdm
8
- from colorama import Fore
9
- from os import makedirs, path, getcwd
10
- from threading import Thread
11
- from sys import stdout
12
- from click import launch as launch_media, confirm as confirm_from_user
13
- import warnings
14
- from webscout.version import __prog__, __version__
15
- from os import getcwd, remove
16
- import appdirs
17
- """
18
- - query string
19
- - format mp4/3
20
- - quality 720p/128kbps
21
- - keywords
22
- - Specify video author
23
- - download related
24
- - max-video limit
25
- - min-video quality
26
- - max-video quality
27
- - path to file containing links
28
- """
29
-
30
- session = requests.session()
31
-
32
- headers = {
33
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
34
- "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36",
35
- "Accept-Encoding": "gzip, deflate, br",
36
- "Accept-Language": "en-US,en;q=0.9",
37
- "referer": "https://y2mate.com",
38
- }
39
-
40
- session.headers.update(headers)
41
-
42
- get_excep = lambda e: e.args[1] if len(e.args) > 1 else e
43
-
44
- appdir = appdirs.AppDirs(__prog__)
45
-
46
- if not path.isdir(appdir.user_cache_dir):
47
- try:
48
- makedirs(appdir.user_cache_dir)
49
- except Exception as e:
50
- print(
51
- f"Error : {get_excep(e)} while creating site directory - "
52
- + appdir.user_cache_dir
53
- )
54
-
55
- history_path = path.join(appdir.user_cache_dir, "history.json")
56
-
57
-
58
- class utils:
59
- @staticmethod
60
- def error_handler(resp=None, exit_on_error=False, log=True):
61
- r"""Execption handler decorator"""
62
-
63
- def decorator(func):
64
- def main(*args, **kwargs):
65
- try:
66
- try:
67
- return func(*args, **kwargs)
68
- except KeyboardInterrupt as e:
69
- print()
70
- logging.info(f"^KeyboardInterrupt quitting. Goodbye!")
71
- exit(1)
72
- except Exception as e:
73
- if log:
74
- # logging.exception(e)
75
- logging.debug(f"Function ({func.__name__}) : {get_excep(e)}")
76
- logging.error(get_excep(e))
77
- if exit_on_error:
78
- exit(1)
79
-
80
- return resp
81
-
82
- return main
83
-
84
- return decorator
85
-
86
- @staticmethod
87
- def get(*args, **kwargs):
88
- r"""Sends http get request"""
89
- resp = session.get(*args, **kwargs)
90
- return all([resp.ok, "application/json" in resp.headers["content-type"]]), resp
91
-
92
- @staticmethod
93
- def post(*args, **kwargs):
94
- r"""Sends http post request"""
95
- resp = session.post(*args, **kwargs)
96
- return all([resp.ok, "application/json" in resp.headers["content-type"]]), resp
97
-
98
- @staticmethod
99
- def add_history(data: dict) -> None:
100
- f"""Adds entry to history
101
- :param data: Response of `third query`
102
- :type data: dict
103
- :rtype: None
104
- """
105
- try:
106
- if not path.isfile(history_path):
107
- data1 = {__prog__: []}
108
- with open(history_path, "w") as fh:
109
- json.dump(data1, fh)
110
- with open(history_path) as fh:
111
- saved_data = json.load(fh).get(__prog__)
112
- data["datetime"] = datetime.now().strftime("%c")
113
- saved_data.append(data)
114
- with open(history_path, "w") as fh:
115
- json.dump({__prog__: saved_data}, fh, indent=4)
116
- except Exception as e:
117
- logging.error(f"Failed to add to history - {get_excep(e)}")
118
-
119
- @staticmethod
120
- def get_history(dump: bool = False) -> list:
121
- r"""Loads download history
122
- :param dump: (Optional) Return whole history as str
123
- :type dump: bool
124
- :rtype: list|str
125
- """
126
- try:
127
- resp = []
128
- if not path.isfile(history_path):
129
- data1 = {__prog__: []}
130
- with open(history_path, "w") as fh:
131
- json.dump(data1, fh)
132
- with open(history_path) as fh:
133
- if dump:
134
- return json.dumps(json.load(fh), indent=4)
135
- entries = json.load(fh).get(__prog__)
136
- for entry in entries:
137
- resp.append(entry.get("vid"))
138
- return resp
139
- except Exception as e:
140
- logging.error(f"Failed to load history - {get_excep(e)}")
141
- return []
142
-
143
-
144
- class first_query:
145
- def __init__(self, query: str):
146
- r"""Initializes first query class
147
- :param query: Video name or youtube link
148
- :type query: str
149
- """
150
- self.query_string = query
151
- self.url = "https://www.y2mate.com/mates/analyzeV2/ajax"
152
- self.payload = self.__get_payload()
153
- self.processed = False
154
- self.is_link = False
155
-
156
- def __get_payload(self):
157
- return {
158
- "hl": "en",
159
- "k_page": "home",
160
- "k_query": self.query_string,
161
- "q_auto": "0",
162
- }
163
-
164
- def __str__(self):
165
- return """
166
- {
167
- "page": "search",
168
- "status": "ok",
169
- "keyword": "happy birthday",
170
- "vitems": [
171
- {
172
- "v": "_z-1fTlSDF0",
173
- "t": "Happy Birthday song"
174
- },
175
- ]
176
- }"""
177
-
178
- def __enter__(self, *args, **kwargs):
179
- return self.__call__(*args, **kwargs)
180
-
181
- def __exit__(self, *args, **kwargs):
182
- self.processed = False
183
-
184
- def __call__(self, timeout: int = 30):
185
- return self.main(timeout)
186
-
187
- def main(self, timeout=30):
188
- r"""Sets class attributes
189
- :param timeout: (Optional) Http requests timeout
190
- :type timeout: int
191
- """
192
- logging.debug(f"Making first query : {self.payload.get('k_query')}")
193
- okay_status, resp = utils.post(self.url, data=self.payload, timeout=timeout)
194
- # print(resp.headers["content-type"])
195
- # print(resp.content)
196
- if okay_status:
197
- dict_data = resp.json()
198
- self.__setattr__("raw", dict_data)
199
- for key in dict_data.keys():
200
- self.__setattr__(key, dict_data.get(key))
201
- self.is_link = not hasattr(self, "vitems")
202
- self.processed = True
203
- else:
204
- logging.debug(f"{resp.headers.get('content-type')} - {resp.content}")
205
- logging.error(f"First query failed - [{resp.status_code} : {resp.reason}")
206
- return self
207
-
208
-
209
- class second_query:
210
- def __init__(self, query_one: object, item_no: int = 0):
211
- r"""Initializes second_query class
212
- :param query_one: Query_one class
213
- :type query_one: object
214
- :param item_no: (Optional) Query_one.vitems index
215
- :type item_no: int
216
- """
217
- assert query_one.processed, "First query failed"
218
-
219
- self.query_one = query_one
220
- self.item_no = item_no
221
- self.processed = False
222
- self.video_dict = None
223
- self.url = "https://www.y2mate.com/mates/analyzeV2/ajax"
224
- # self.payload = self.__get_payload()
225
-
226
- def __str__(self):
227
- return """
228
- {
229
- "status": "ok",
230
- "mess": "",
231
- "page": "detail",
232
- "vid": "_z-1fTlSDF0",
233
- "extractor": "youtube",
234
- "title": "Happy Birthday song",
235
- "t": 62,
236
- "a": "infobells",
237
- "links": {
238
- "mp4": {
239
- "136": {
240
- "size": "5.5 MB",
241
- "f": "mp4",
242
- "q": "720p",
243
- "q_text": "720p (.mp4) <span class=\"label label-primary\"><small>m-HD</small></span>",
244
- "k": "joVBVdm2xZWhaZWhu6vZ8cXxAl7j4qpyhNgqkwx0U/tcutx/harxdZ8BfPNcg9n1"
245
- },
246
- },
247
- "mp3": {
248
- "140": {
249
- "size": "975.1 KB",
250
- "f": "m4a",
251
- "q": ".m4a",
252
- "q_text": ".m4a (128kbps)",
253
- "k": "joVBVdm2xZWhaZWhu6vZ8cXxAl7j4qpyhNhuxgxyU/NQ9919mbX2dYcdevRBnt0="
254
- },
255
- },
256
- "related": [
257
- {
258
- "title": "Related Videos",
259
- "contents": [
260
- {
261
- "v": "KK24ZvxLXGU",
262
- "t": "Birthday Songs - Happy Birthday To You | 15 minutes plus"
263
- },
264
- ]
265
- }
266
- ]
267
- }
268
- """
269
-
270
- def __call__(self, *args, **kwargs):
271
- return self.main(*args, **kwargs)
272
-
273
- def get_item(self, item_no=0):
274
- r"""Return specific items on `self.query_one.vitems`"""
275
- if self.video_dict:
276
- return self.video_dict
277
- if self.query_one.is_link:
278
- return {"v": self.query_one.vid, "t": self.query_one.title}
279
- all_items = self.query_one.vitems
280
- assert (
281
- self.item_no < len(all_items) - 1
282
- ), "The item_no is greater than largest item's index - try lower value"
283
-
284
- return self.query_one.vitems[item_no or self.item_no]
285
-
286
- def get_payload(self):
287
- return {
288
- "hl": "en",
289
- "k_page": "home",
290
- "k_query": f"https://www.youtube.com/watch?v={self.get_item().get('v')}",
291
- "q_auto": "1",
292
- }
293
-
294
- def __main__(self, *args, **kwargs):
295
- return self.main(*args, **kwargs)
296
-
297
- def __enter__(self, *args, **kwargs):
298
- return self.__main__(*args, **kwargs)
299
-
300
- def __exit__(self, *args, **kwargs):
301
- self.processed = False
302
-
303
- def main(self, item_no: int = 0, timeout: int = 30):
304
- r"""Requests for video formats and related videos
305
- :param item_no: (Optional) Index of query_one.vitems
306
- :type item_no: int
307
- :param timeout: (Optional)Http request timeout
308
- :type timeout: int
309
- """
310
- self.processed = False
311
- if item_no:
312
- self.item_no = item_no
313
- okay_status, resp = utils.post(
314
- self.url, data=self.get_payload(), timeout=timeout
315
- )
316
-
317
- if okay_status:
318
- dict_data = resp.json()
319
- for key in dict_data.keys():
320
- self.__setattr__(key, dict_data.get(key))
321
- links = dict_data.get("links")
322
- self.__setattr__("video", links.get("mp4"))
323
- self.__setattr__("audio", links.get("mp3"))
324
- self.__setattr__("related", dict_data.get("related")[0].get("contents"))
325
- self.__setattr__("raw", dict_data)
326
- self.processed = True
327
-
328
- else:
329
- logging.debug(f"{resp.headers.get('content-type')} - {resp.content}")
330
- logging.error(f"Second query failed - [{resp.status_code} : {resp.reason}]")
331
- return self
332
-
333
-
334
- class third_query:
335
- def __init__(self, query_two: object):
336
- assert query_two.processed, "Unprocessed second_query object parsed"
337
- self.query_two = query_two
338
- self.url = "https://www.y2mate.com/mates/convertV2/index"
339
- self.formats = ["mp4", "mp3"]
340
- self.qualities_plus = ["best", "worst"]
341
- self.qualities = {
342
- self.formats[0]: [
343
- "4k",
344
- "1080p",
345
- "720p",
346
- "480p",
347
- "360p",
348
- "240p",
349
- "144p",
350
- "auto",
351
- ]
352
- + self.qualities_plus,
353
- self.formats[1]: ["mp3", "m4a", ".m4a", "128kbps", "192kbps", "328kbps"],
354
- }
355
-
356
- def __call__(self, *args, **kwargs):
357
- return self.main(*args, **kwargs)
358
-
359
- def __enter__(self, *args, **kwargs):
360
- return self
361
-
362
- def __exit__(self, *args, **kwargs):
363
- pass
364
-
365
- def __str__(self):
366
- return """
367
- {
368
- "status": "ok",
369
- "mess": "",
370
- "c_status": "CONVERTED",
371
- "vid": "_z-1fTlSDF0",
372
- "title": "Happy Birthday song",
373
- "ftype": "mp4",
374
- "fquality": "144p",
375
- "dlink": "https://dl165.dlmate13.online/?file=M3R4SUNiN3JsOHJ6WWQ2a3NQS1Y5ZGlxVlZIOCtyZ01tY1VxM2xzQkNMbFlyb2t1enErekxNZElFYkZlbWQ2U1g5TkVvWGplZU55T0R4K0lvcEI3QnlHbjd0a29yU3JOOXN0eWY4UmhBbE9xdmI3bXhCZEprMHFrZU96QkpweHdQVWh0OGhRMzQyaWUzS1dTdmhEMzdsYUk0VWliZkMwWXR5OENNUENOb01rUWd6NmJQS2UxaGRZWHFDQ2c0WkpNMmZ2QTVVZmx5cWc3NVlva0Nod3NJdFpPejhmeDNhTT0%3D"
376
- }
377
- """
378
-
379
- def get_payload(self, keys):
380
- return {"k": keys.get("k"), "vid": self.query_two.vid}
381
-
382
- def main(
383
- self,
384
- format: str = "mp4",
385
- quality="auto",
386
- resolver: str = None,
387
- timeout: int = 30,
388
- ):
389
- r"""
390
- :param format: (Optional) Media format mp4/mp3
391
- :param quality: (Optional) Media qualiy such as 720p
392
- :param resolver: (Optional) Additional format info : [m4a,3gp,mp4,mp3]
393
- :param timeout: (Optional) Http requests timeout
394
- :type type: str
395
- :type quality: str
396
- :type timeout: int
397
- """
398
- if not resolver:
399
- resolver = "mp4" if format == "mp4" else "mp3"
400
- if format == "mp3" and quality == "auto":
401
- quality = "128kbps"
402
- assert (
403
- format in self.formats
404
- ), f"'{format}' is not in supported formats - {self.formats}"
405
-
406
- assert (
407
- quality in self.qualities[format]
408
- ), f"'{quality}' is not in supported qualities - {self.qualities[format]}"
409
-
410
- items = self.query_two.video if format == "mp4" else self.query_two.audio
411
- hunted = []
412
- if quality in self.qualities_plus:
413
- keys = list(items.keys())
414
- if quality == self.qualities_plus[0]:
415
- hunted.append(items[keys[0]])
416
- else:
417
- hunted.append(items[keys[len(keys) - 2]])
418
- else:
419
- for key in items.keys():
420
- if items[key].get("q") == quality:
421
- hunted.append(items[key])
422
- if len(hunted) > 1:
423
- for entry in hunted:
424
- if entry.get("f") == resolver:
425
- hunted.insert(0, entry)
426
- if hunted:
427
-
428
- def hunter_manager(souped_entry: dict = hunted[0], repeat_count=0):
429
- payload = self.get_payload(souped_entry)
430
- okay_status, resp = utils.post(self.url, data=payload)
431
- if okay_status:
432
- sanitized_feedback = resp.json()
433
- if sanitized_feedback.get("c_status") == "CONVERTING":
434
- if repeat_count >= 4:
435
- return (False, {})
436
- else:
437
- logging.debug(
438
- f"Converting video : sleeping for 5s - round {repeat_count+1}"
439
- )
440
- sleep(5)
441
- repeat_count += 1
442
- return hunter_manager(souped_entry)
443
- return okay_status, resp
444
- return okay_status, resp
445
-
446
- okay_status, resp = hunter_manager()
447
-
448
- if okay_status:
449
- resp_data = hunted[0]
450
- resp_data.update(resp.json())
451
- return resp_data
452
-
453
- else:
454
- logging.debug(f"{resp.headers.get('content-type')} - {resp.content}")
455
- logging.error(
456
- f"Third query failed - [{resp.status_code} : {resp.reason}]"
457
- )
458
- return {}
459
- else:
460
- logging.error(
461
- f"Zero media hunted with params : {{quality : {quality}, format : {format} }}"
462
- )
463
- return {}
464
- class Handler:
465
- def __init__(
466
- self,
467
- query: str,
468
- author: str = None,
469
- timeout: int = 30,
470
- confirm: bool = False,
471
- unique: bool = False,
472
- thread: int = 0,
473
- ):
474
- r"""Initializes this `class`
475
- :param query: Video name or youtube link
476
- :type query: str
477
- :param author: (Optional) Author (Channel) of the videos
478
- :type author: str
479
- :param timeout: (Optional) Http request timeout
480
- :type timeout: int
481
- :param confirm: (Optional) Confirm before downloading media
482
- :type confirm: bool
483
- :param unique: (Optional) Ignore previously downloaded media
484
- :type confirm: bool
485
- :param thread: (Optional) Thread the download process through `auto-save` method
486
- :type thread int
487
- """
488
- self.query = query
489
- self.author = author
490
- self.timeout = timeout
491
- self.keyword = None
492
- self.confirm = confirm
493
- self.unique = unique
494
- self.thread = thread
495
- self.vitems = []
496
- self.related = []
497
- self.dropped = []
498
- self.total = 1
499
- self.saved_videos = utils.get_history()
500
-
501
- def __str__(self):
502
- return self.query
503
-
504
- def __enter__(self, *args, **kwargs):
505
- return self
506
-
507
- def __exit__(self, *args, **kwargs):
508
- self.vitems.clear()
509
- self.total = 1
510
-
511
- def __call__(self, *args, **kwargs):
512
- return self.run(*args, **kwargs)
513
-
514
- def __filter_videos(self, entries: list) -> list:
515
- f"""Filter videos based on keyword
516
- :param entries: List containing dict of video id and their titles
517
- :type entries: list
518
- :rtype: list
519
- """
520
- if self.keyword:
521
- keyword = self.keyword.lower()
522
- resp = []
523
- for entry in entries:
524
- if keyword in entry.get("t").lower():
525
- resp.append(entry)
526
- return resp
527
-
528
- else:
529
- return entries
530
-
531
- def __make_first_query(self):
532
- r"""Sets query_one attribute to `self`"""
533
- query_one = first_query(self.query)
534
- self.__setattr__("query_one", query_one.main(self.timeout))
535
- if self.query_one.is_link == False:
536
- self.vitems.extend(self.__filter_videos(self.query_one.vitems))
537
-
538
- @utils.error_handler(exit_on_error=True)
539
- def __verify_item(self, second_query_obj) -> bool:
540
- video_id = second_query_obj.vid
541
- video_author = second_query_obj.a
542
- video_title = second_query_obj.title
543
- if video_id in self.saved_videos:
544
- if self.unique:
545
- return False, "Duplicate"
546
- if self.confirm:
547
- choice = confirm_from_user(
548
- f">> Re-download : {Fore.GREEN+video_title+Fore.RESET} by {Fore.YELLOW+video_author+Fore.RESET}"
549
- )
550
- print("\n[*] Ok processing...", end="\r")
551
- return choice, "User's choice"
552
- if self.confirm:
553
- choice = confirm_from_user(
554
- f">> Download : {Fore.GREEN+video_title+Fore.RESET} by {Fore.YELLOW+video_author+Fore.RESET}"
555
- )
556
- print("\n[*] Ok processing...", end="\r")
557
- return choice, "User's choice"
558
- return True, "Auto"
559
-
560
- def __make_second_query(self):
561
- r"""Links first query with 3rd query"""
562
- init_query_two = second_query(self.query_one)
563
- x = 0
564
- if not self.query_one.is_link:
565
- for video_dict in self.vitems:
566
- init_query_two.video_dict = video_dict
567
- query_2 = init_query_two.main(timeout=self.timeout)
568
- if query_2.processed:
569
- if query_2.vid in self.dropped:
570
- continue
571
- if self.author and not self.author.lower() in query_2.a.lower():
572
- logging.warning(
573
- f"Dropping {Fore.YELLOW+query_2.title+Fore.RESET} by {Fore.RED+query_2.a+Fore.RESET}"
574
- )
575
- continue
576
- else:
577
- yes_download, reason = self.__verify_item(query_2)
578
- if not yes_download:
579
- logging.warning(
580
- f"Skipping {Fore.YELLOW+query_2.title+Fore.RESET} by {Fore.MAGENTA+query_2.a+Fore.RESET} - Reason : {Fore.BLUE+reason+Fore.RESET}"
581
- )
582
- self.dropped.append(query_2.vid)
583
- continue
584
- self.related.append(query_2.related)
585
- yield query_2
586
- x += 1
587
- if x >= self.total:
588
- break
589
- else:
590
- logging.warning(
591
- f"Dropping unprocessed query_two object of index {x}"
592
- )
593
-
594
- else:
595
- query_2 = init_query_two.main(timeout=self.timeout)
596
- if query_2.processed:
597
- # self.related.extend(query_2.related)
598
- self.vitems.extend(query_2.related)
599
- self.query_one.is_link = False
600
- if self.total == 1:
601
- yield query_2
602
- else:
603
- for video_dict in self.vitems:
604
- init_query_two.video_dict = video_dict
605
- query_2 = init_query_two.main(timeout=self.timeout)
606
- if query_2.processed:
607
- if (
608
- self.author
609
- and not self.author.lower() in query_2.a.lower()
610
- ):
611
- logging.warning(
612
- f"Dropping {Fore.YELLOW+query_2.title+Fore.RESET} by {Fore.RED+query_2.a+Fore.RESET}"
613
- )
614
- continue
615
- else:
616
- yes_download, reason = self.__verify_item(query_2)
617
- if not yes_download:
618
- logging.warning(
619
- f"Skipping {Fore.YELLOW+query_2.title+Fore.RESET} by {Fore.MAGENTA+query_2.a+Fore.RESET} - Reason : {Fore.BLUE+reason+Fore.RESET}"
620
- )
621
- self.dropped.append(query_2.vid)
622
- continue
623
-
624
- self.related.append(query_2.related)
625
- yield query_2
626
- x += 1
627
- if x >= self.total:
628
- break
629
- else:
630
- logging.warning(
631
- f"Dropping unprocessed query_two object of index {x}"
632
- )
633
- yield
634
- else:
635
- logging.warning("Dropping unprocessed query_two object")
636
- yield
637
-
638
- def run(
639
- self,
640
- format: str = "mp4",
641
- quality: str = "auto",
642
- resolver: str = None,
643
- limit: int = 1,
644
- keyword: str = None,
645
- author: str = None,
646
- ):
647
- r"""Generate and yield video dictionary
648
- :param format: (Optional) Media format mp4/mp3
649
- :param quality: (Optional) Media qualiy such as 720p/128kbps
650
- :param resolver: (Optional) Additional format info : [m4a,3gp,mp4,mp3]
651
- :param limit: (Optional) Total videos to be generated
652
- :param keyword: (Optional) Video keyword
653
- :param author: (Optional) Author of the videos
654
- :type quality: str
655
- :type total: int
656
- :type keyword: str
657
- :type author: str
658
- :rtype: object
659
- """
660
- self.author = author
661
- self.keyword = keyword
662
- self.total = limit
663
- self.__make_first_query()
664
- for query_two_obj in self.__make_second_query():
665
- if query_two_obj:
666
- self.vitems.extend(query_two_obj.related)
667
- yield third_query(query_two_obj).main(
668
- **dict(
669
- format=format,
670
- quality=quality,
671
- resolver=resolver,
672
- timeout=self.timeout,
673
- )
674
- )
675
- else:
676
- logging.error(f"Empty object - {query_two_obj}")
677
-
678
- def generate_filename(self, third_dict: dict, naming_format: str = None) -> str:
679
- r"""Generate filename based on the response of `third_query`
680
- :param third_dict: response of `third_query.main()` object
681
- :param naming_format: (Optional) Format for generating filename based on `third_dict` keys
682
- :type third_dict: dict
683
- :type naming_format: str
684
- :rtype: str
685
- """
686
- fnm = (
687
- f"{naming_format}" % third_dict
688
- if naming_format
689
- else f"{third_dict['title']} {third_dict['vid']}_{third_dict['fquality']}.{third_dict['ftype']}"
690
- )
691
-
692
- def sanitize(nm):
693
- trash = [
694
- "\\",
695
- "/",
696
- ":",
697
- "*",
698
- "?",
699
- '"',
700
- "<",
701
- "|",
702
- ">",
703
- "y2mate.com",
704
- "y2mate com",
705
- ]
706
- for val in trash:
707
- nm = nm.replace(val, "")
708
- return nm.strip()
709
-
710
- return sanitize(fnm)
711
-
712
- def auto_save(
713
- self,
714
- dir: str = "",
715
- iterator: object = None,
716
- progress_bar=True,
717
- quiet: bool = False,
718
- naming_format: str = None,
719
- chunk_size: int = 512,
720
- play: bool = False,
721
- resume: bool = False,
722
- *args,
723
- **kwargs,
724
- ):
725
- r"""Query and save all the media
726
- :param dir: (Optional) Path to Directory for saving the media files
727
- :param iterator: (Optional) Function that yields third_query object - `Handler.run`
728
- :param progress_bar: (Optional) Display progress bar
729
- :param quiet: (Optional) Not to stdout anything
730
- :param naming_format: (Optional) Format for generating filename
731
- :param chunk_size: (Optional) Chunk_size for downloading files in KB
732
- :param play: (Optional) Auto-play the media after download
733
- :param resume: (Optional) Resume the incomplete download
734
- :type dir: str
735
- :type iterator: object
736
- :type progress_bar: bool
737
- :type quiet: bool
738
- :type naming_format: str
739
- :type chunk_size: int
740
- :type play: bool
741
- :type resume: bool
742
- args & kwargs for the iterator
743
- :rtype: None
744
- """
745
- iterator_object = iterator or self.run(*args, **kwargs)
746
-
747
- for x, entry in enumerate(iterator_object):
748
- if self.thread:
749
- t1 = Thread(
750
- target=self.save,
751
- args=(
752
- entry,
753
- dir,
754
- False,
755
- quiet,
756
- naming_format,
757
- chunk_size,
758
- play,
759
- resume,
760
- ),
761
- )
762
- t1.start()
763
- thread_count = x + 1
764
- if thread_count % self.thread == 0 or thread_count == self.total:
765
- logging.debug(
766
- f"Waiting for current running threads to finish - thread_count : {thread_count}"
767
- )
768
- t1.join()
769
- else:
770
- self.save(
771
- entry,
772
- dir,
773
- progress_bar,
774
- quiet,
775
- naming_format,
776
- chunk_size,
777
- play,
778
- resume,
779
- )
780
-
781
- def save(
782
- self,
783
- third_dict: dict,
784
- dir: str = "",
785
- progress_bar=True,
786
- quiet: bool = False,
787
- naming_format: str = None,
788
- chunk_size: int = 512,
789
- play: bool = False,
790
- resume: bool = False,
791
- disable_history=False,
792
- ):
793
- r"""Download media based on response of `third_query` dict-data-type
794
- :param third_dict: Response of `third_query.run()`
795
- :param dir: (Optional) Directory for saving the contents
796
- :param progress_bar: (Optional) Display download progress bar
797
- :param quiet: (Optional) Not to stdout anything
798
- :param naming_format: (Optional) Format for generating filename
799
- :param chunk_size: (Optional) Chunk_size for downloading files in KB
800
- :param play: (Optional) Auto-play the media after download
801
- :param resume: (Optional) Resume the incomplete download
802
- :param disable_history (Optional) Don't save the download to history.
803
- :type third_dict: dict
804
- :type dir: str
805
- :type progress_bar: bool
806
- :type quiet: bool
807
- :type naming_format: str
808
- :type chunk_size: int
809
- :type play: bool
810
- :type resume: bool
811
- :type disable_history: bool
812
- :rtype: None
813
- """
814
- if third_dict:
815
- assert third_dict.get(
816
- "dlink"
817
- ), "The video selected does not support that quality, try lower qualities."
818
- if third_dict.get("mess"):
819
- logging.warning(third_dict.get("mess"))
820
-
821
- current_downloaded_size = 0
822
- current_downloaded_size_in_mb = 0
823
- filename = self.generate_filename(third_dict, naming_format)
824
- save_to = path.join(dir, filename)
825
- mod_headers = headers
826
-
827
- if resume:
828
- assert path.exists(save_to), f"File not found in path - '{save_to}'"
829
- current_downloaded_size = path.getsize(save_to)
830
- # Set the headers to resume download from the last byte
831
- mod_headers = {"Range": f"bytes={current_downloaded_size}-"}
832
- current_downloaded_size_in_mb = round(
833
- current_downloaded_size / 1000000, 2
834
- ) # convert to mb
835
-
836
- resp = requests.get(third_dict["dlink"], stream=True, headers=mod_headers)
837
-
838
- default_content_length = 0
839
- size_in_bytes = int(
840
- resp.headers.get("content-length", default_content_length)
841
- )
842
- if not size_in_bytes:
843
- if resume:
844
- raise FileExistsError(
845
- f"Download completed for the file in path - '{save_to}'"
846
- )
847
- else:
848
- raise Exception(
849
- f"Cannot download file of content-length {size_in_bytes} bytes"
850
- )
851
-
852
- if resume:
853
- assert (
854
- size_in_bytes != current_downloaded_size
855
- ), f"Download completed for the file in path - '{save_to}'"
856
-
857
- size_in_mb = (
858
- round(size_in_bytes / 1000000, 2) + current_downloaded_size_in_mb
859
- )
860
- chunk_size_in_bytes = chunk_size * 1024
861
-
862
- third_dict["saved_to"] = (
863
- save_to
864
- if any([save_to.startswith("/"), ":" in save_to])
865
- else path.join(getcwd(), dir, filename)
866
- )
867
- try_play_media = (
868
- lambda: launch_media(third_dict["saved_to"]) if play else None
869
- )
870
- saving_mode = "ab" if resume else "wb"
871
- if progress_bar:
872
- if not quiet:
873
- print(f"{filename}")
874
- with tqdm(
875
- total=size_in_bytes + current_downloaded_size,
876
- bar_format="%s%d MB %s{bar} %s{l_bar}%s"
877
- % (Fore.GREEN, size_in_mb, Fore.CYAN, Fore.YELLOW, Fore.RESET),
878
- initial=current_downloaded_size,
879
- ) as p_bar:
880
- # p_bar.update(current_downloaded_size)
881
- with open(save_to, saving_mode) as fh:
882
- for chunks in resp.iter_content(chunk_size=chunk_size_in_bytes):
883
- fh.write(chunks)
884
- p_bar.update(chunk_size_in_bytes)
885
- if not disable_history:
886
- utils.add_history(third_dict)
887
- try_play_media()
888
- return save_to
889
- else:
890
- with open(save_to, saving_mode) as fh:
891
- for chunks in resp.iter_content(chunk_size=chunk_size_in_bytes):
892
- fh.write(chunks)
893
- if not disable_history:
894
- utils.add_history(third_dict)
895
-
896
- try_play_media()
897
- logging.info(f"{filename} - {size_in_mb}MB ✅")
898
- return save_to
899
- else:
900
- logging.error(f"Empty `third_dict` parameter parsed : {third_dict}")
901
-
902
- mp4_qualities = [
903
- "4k",
904
- "1080p",
905
- "720p",
906
- "480p",
907
- "360p",
908
- "240p",
909
- "144p",
910
- "auto",
911
- "best",
912
- "worst",
913
- ]
914
- mp3_qualities = ["mp3", "m4a", ".m4a", "128kbps", "192kbps", "328kbps"]
915
- resolvers = ["m4a", "3gp", "mp4", "mp3"]
916
- media_qualities = mp4_qualities + mp3_qualities
917
- logging.basicConfig(
918
- format="%(asctime)s - %(levelname)s : %(message)s",
919
- datefmt="%H:%M:%S",
920
- level=logging.INFO,
921
- )
922
-
923
-
924
- def get_args():
925
- parser = argparse.ArgumentParser(
926
- description="Youtube video downloader", add_help=True, exit_on_error=True
927
- )
928
- parser.add_argument(
929
- "-v", "--version", action="version", version=f"%(prog)s v{__version__}"
930
- )
931
- parser.add_argument(
932
- "query", nargs="*", help="Youtube video title, link or id - %(default)s"
933
- )
934
- parser.add_argument(
935
- "-f",
936
- "--format",
937
- help="Specify media type - audio/video",
938
- choices=["mp3", "mp4"],
939
- metavar="mp3|mp4",
940
- )
941
- parser.add_argument(
942
- "-q",
943
- "--quality",
944
- help="Media quality - %(default)s",
945
- choices=media_qualities,
946
- metavar="|".join(media_qualities),
947
- default="auto",
948
- )
949
- parser.add_argument(
950
- "-r",
951
- "--resolver",
952
- help="Other media formats incase of multiple options - mp4/mp3",
953
- choices=resolvers,
954
- metavar="|".join(resolvers),
955
- )
956
- parser.add_argument(
957
- "-k",
958
- "--keyword",
959
- nargs="*",
960
- help="Media should contain this keywords - %(default)s",
961
- )
962
- parser.add_argument(
963
- "-a",
964
- "--author",
965
- nargs="*",
966
- help="Media author i.e YouTube channel name - %(default)s",
967
- )
968
- parser.add_argument(
969
- "-l",
970
- "--limit",
971
- help="Total videos to be downloaded - %(default)s",
972
- type=int,
973
- default=1,
974
- )
975
- parser.add_argument(
976
- "-d",
977
- "--dir",
978
- help="Directory for saving the contents - %(default)s",
979
- default=getcwd(),
980
- metavar="PATH",
981
- )
982
- parser.add_argument(
983
- "-t",
984
- "--timeout",
985
- help="Http request timeout in seconds - %(default)s",
986
- type=int,
987
- default=30,
988
- )
989
- parser.add_argument(
990
- "-c",
991
- "--chunk",
992
- help="Chunk-size for downloading files in KB - %(default)s",
993
- type=int,
994
- default=256,
995
- )
996
- parser.add_argument(
997
- "-i",
998
- "--input",
999
- help="Path to text file containing query per line - %(default)s",
1000
- metavar="PATH",
1001
- )
1002
- parser.add_argument(
1003
- "-o",
1004
- "--output",
1005
- metavar="FORMAT",
1006
- help="Format for generating filename %%(key)s : [title,vid,fquality,ftype] or 'pretty' - %(default)s",
1007
- )
1008
- parser.add_argument(
1009
- "-thr",
1010
- "--thread",
1011
- help="Download [x] amount of videos/audios at once - 1",
1012
- type=int,
1013
- default=0,
1014
- )
1015
- parser.add_argument(
1016
- "--disable-bar",
1017
- help="Disable download progress bar - %(default)s",
1018
- action="store_true",
1019
- )
1020
- parser.add_argument(
1021
- "--confirm",
1022
- help="Confirm before downloading file - %(default)s",
1023
- action="store_true",
1024
- )
1025
- parser.add_argument(
1026
- "--unique",
1027
- help="Auto-skip any media that you once dowloaded - %(default)s",
1028
- action="store_true",
1029
- )
1030
- parser.add_argument(
1031
- "--quiet",
1032
- help="Not to stdout anything other than logs - %(default)s",
1033
- action="store_true",
1034
- )
1035
- parser.add_argument(
1036
- "--history",
1037
- help="Stdout all media metadata ever downloaded - %(default)s",
1038
- action="store_true",
1039
- )
1040
- parser.add_argument(
1041
- "--clear",
1042
- help="Clear all download histories - %(default)s",
1043
- action="store_true",
1044
- )
1045
- parser.add_argument(
1046
- "--resume", action="store_true", help="Resume downloading incomplete downloads"
1047
- )
1048
- parser.add_argument(
1049
- "--play", help="Play media after download - %(default)s", action="store_true"
1050
- )
1051
- return parser.parse_args()
1052
-
1053
-
1054
- @utils.error_handler(exit_on_error=True)
1055
- def main():
1056
- args = get_args()
1057
- if args.history:
1058
- print(utils.get_history(dump=True))
1059
- exit(0)
1060
- if args.clear:
1061
- remove(history_path)
1062
- logging.info("Histories cleared successfully!")
1063
- exit(0)
1064
- if not args.format:
1065
- raise Exception("You must specify media format [ -f mp3/4]")
1066
- h_mult_args = lambda v: v if not v else " ".join(v)
1067
- handler_init_args = dict(
1068
- query=h_mult_args(args.query),
1069
- author=args.author,
1070
- timeout=args.timeout,
1071
- confirm=args.confirm,
1072
- unique=args.unique,
1073
- thread=args.thread,
1074
- )
1075
- auto_save_args = dict(
1076
- dir=args.dir,
1077
- progress_bar=args.disable_bar == False,
1078
- quiet=args.quiet,
1079
- naming_format=f"%(title)s{' - %(fquality)s' if args.format=='mp4' else ''}.%(ftype)s"
1080
- if str(args.output).lower() == "pretty"
1081
- else args.output,
1082
- chunk_size=args.chunk,
1083
- play=args.play,
1084
- format=args.format,
1085
- quality=args.quality,
1086
- resolver=args.resolver,
1087
- limit=args.limit,
1088
- keyword=h_mult_args(args.keyword),
1089
- author=h_mult_args(args.author),
1090
- resume=args.resume,
1091
- )
1092
- logging.info(f"webscout - v{__version__}")
1093
- if args.input:
1094
- for query in open(args.input).read().strip().split("\n"):
1095
- handler_init_args["query"] = query
1096
- auto_save_args["limit"] = 1
1097
- Handler(**handler_init_args).auto_save(**auto_save_args)
1098
- else:
1099
- Handler(**handler_init_args).auto_save(**auto_save_args)
1100
- logging.info(
1101
- f"Done downloading ({args.limit}) {'audio' if args.format=='mp3' else 'video'}{'' if args.limit==1 else 's'}"
1102
- )
1103
- if __name__ == "__main__":
1
+ from datetime import datetime
2
+ import json
3
+ from webscout.Litlogger import LitLogger
4
+ from webscout.litagent import LitAgent
5
+ from time import sleep
6
+ import requests
7
+ from tqdm import tqdm
8
+ from colorama import Fore
9
+ from os import makedirs, path, getcwd, remove
10
+ from threading import Thread
11
+ from sys import stdout
12
+ import os
13
+ import subprocess
14
+ import sys
15
+ from webscout.version import __prog__, __version__
16
+ from webscout.zerodir import user_cache_dir
17
+ from webscout.swiftcli import CLI, option, argument, group
18
+
19
+ logging = LitLogger(name="YTDownloader")
20
+
21
+ session = requests.session()
22
+
23
+ headers = {
24
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
25
+ "User-Agent": LitAgent().random(),
26
+ "Accept-Encoding": "gzip, deflate, br",
27
+ "Accept-Language": "en-US,en;q=0.9",
28
+ "referer": "https://y2mate.com",
29
+ }
30
+
31
+ session.headers.update(headers)
32
+
33
+ get_excep = lambda e: e.args[1] if len(e.args) > 1 else e
34
+
35
+ appdir = user_cache_dir(__prog__, __prog__)
36
+
37
+ if not path.isdir(appdir):
38
+ try:
39
+ makedirs(appdir)
40
+ except Exception as e:
41
+ print(
42
+ f"Error : {get_excep(e)} while creating site directory - "
43
+ + appdir
44
+ )
45
+
46
+ history_path = path.join(appdir, "history.json")
47
+
48
+
49
+ class utils:
50
+ @staticmethod
51
+ def error_handler(resp=None, exit_on_error=False, log=True):
52
+ r"""Execption handler decorator"""
53
+
54
+ def decorator(func):
55
+ def main(*args, **kwargs):
56
+ try:
57
+ try:
58
+ return func(*args, **kwargs)
59
+ except KeyboardInterrupt as e:
60
+ print()
61
+ logging.info(f"^KeyboardInterrupt quitting. Goodbye!")
62
+ exit(1)
63
+ except Exception as e:
64
+ if log:
65
+ # logging.exception(e)
66
+ logging.debug(f"Function ({func.__name__}) : {get_excep(e)}")
67
+ logging.error(get_excep(e))
68
+ if exit_on_error:
69
+ exit(1)
70
+
71
+ return resp
72
+
73
+ return main
74
+
75
+ return decorator
76
+
77
+ @staticmethod
78
+ def get(*args, **kwargs):
79
+ r"""Sends http get request"""
80
+ resp = session.get(*args, **kwargs)
81
+ return all([resp.ok, "application/json" in resp.headers["content-type"]]), resp
82
+
83
+ @staticmethod
84
+ def post(*args, **kwargs):
85
+ r"""Sends http post request"""
86
+ resp = session.post(*args, **kwargs)
87
+ return all([resp.ok, "application/json" in resp.headers["content-type"]]), resp
88
+
89
+ @staticmethod
90
+ def add_history(data: dict) -> None:
91
+ f"""Adds entry to history
92
+ :param data: Response of `third query`
93
+ :type data: dict
94
+ :rtype: None
95
+ """
96
+ try:
97
+ if not path.isfile(history_path):
98
+ data1 = {__prog__: []}
99
+ with open(history_path, "w") as fh:
100
+ json.dump(data1, fh)
101
+ with open(history_path) as fh:
102
+ saved_data = json.load(fh).get(__prog__)
103
+ data["datetime"] = datetime.now().strftime("%c")
104
+ saved_data.append(data)
105
+ with open(history_path, "w") as fh:
106
+ json.dump({__prog__: saved_data}, fh, indent=4)
107
+ except Exception as e:
108
+ logging.error(f"Failed to add to history - {get_excep(e)}")
109
+
110
+ @staticmethod
111
+ def get_history(dump: bool = False) -> list:
112
+ r"""Loads download history
113
+ :param dump: (Optional) Return whole history as str
114
+ :type dump: bool
115
+ :rtype: list|str
116
+ """
117
+ try:
118
+ resp = []
119
+ if not path.isfile(history_path):
120
+ data1 = {__prog__: []}
121
+ with open(history_path, "w") as fh:
122
+ json.dump(data1, fh)
123
+ with open(history_path) as fh:
124
+ if dump:
125
+ return json.dumps(json.load(fh), indent=4)
126
+ entries = json.load(fh).get(__prog__)
127
+ for entry in entries:
128
+ resp.append(entry.get("vid"))
129
+ return resp
130
+ except Exception as e:
131
+ logging.error(f"Failed to load history - {get_excep(e)}")
132
+ return []
133
+
134
+
135
+ class first_query:
136
+ def __init__(self, query: str):
137
+ r"""Initializes first query class
138
+ :param query: Video name or youtube link
139
+ :type query: str
140
+ """
141
+ self.query_string = query
142
+ self.url = "https://www.y2mate.com/mates/analyzeV2/ajax"
143
+ self.payload = self.__get_payload()
144
+ self.processed = False
145
+ self.is_link = False
146
+
147
+ def __get_payload(self):
148
+ return {
149
+ "hl": "en",
150
+ "k_page": "home",
151
+ "k_query": self.query_string,
152
+ "q_auto": "0",
153
+ }
154
+
155
+ def __str__(self):
156
+ return """
157
+ {
158
+ "page": "search",
159
+ "status": "ok",
160
+ "keyword": "happy birthday",
161
+ "vitems": [
162
+ {
163
+ "v": "_z-1fTlSDF0",
164
+ "t": "Happy Birthday song"
165
+ },
166
+ ]
167
+ }"""
168
+
169
+ def __enter__(self, *args, **kwargs):
170
+ return self.__call__(*args, **kwargs)
171
+
172
+ def __exit__(self, *args, **kwargs):
173
+ self.processed = False
174
+
175
+ def __call__(self, timeout: int = 30):
176
+ return self.main(timeout)
177
+
178
+ def main(self, timeout=30):
179
+ r"""Sets class attributes
180
+ :param timeout: (Optional) Http requests timeout
181
+ :type timeout: int
182
+ """
183
+ logging.debug(f"Making first query : {self.payload.get('k_query')}")
184
+ okay_status, resp = utils.post(self.url, data=self.payload, timeout=timeout)
185
+ # print(resp.headers["content-type"])
186
+ # print(resp.content)
187
+ if okay_status:
188
+ dict_data = resp.json()
189
+ self.__setattr__("raw", dict_data)
190
+ for key in dict_data.keys():
191
+ self.__setattr__(key, dict_data.get(key))
192
+ self.is_link = not hasattr(self, "vitems")
193
+ self.processed = True
194
+ else:
195
+ logging.debug(f"{resp.headers.get('content-type')} - {resp.content}")
196
+ logging.error(f"First query failed - [{resp.status_code} : {resp.reason}")
197
+ return self
198
+
199
+
200
+ class second_query:
201
+ def __init__(self, query_one: object, item_no: int = 0):
202
+ r"""Initializes second_query class
203
+ :param query_one: Query_one class
204
+ :type query_one: object
205
+ :param item_no: (Optional) Query_one.vitems index
206
+ :type item_no: int
207
+ """
208
+ assert query_one.processed, "First query failed"
209
+
210
+ self.query_one = query_one
211
+ self.item_no = item_no
212
+ self.processed = False
213
+ self.video_dict = None
214
+ self.url = "https://www.y2mate.com/mates/analyzeV2/ajax"
215
+ # self.payload = self.__get_payload()
216
+
217
+ def __str__(self):
218
+ return """
219
+ {
220
+ "status": "ok",
221
+ "mess": "",
222
+ "page": "detail",
223
+ "vid": "_z-1fTlSDF0",
224
+ "extractor": "youtube",
225
+ "title": "Happy Birthday song",
226
+ "t": 62,
227
+ "a": "infobells",
228
+ "links": {
229
+ "mp4": {
230
+ "136": {
231
+ "size": "5.5 MB",
232
+ "f": "mp4",
233
+ "q": "720p",
234
+ "q_text": "720p (.mp4) <span class=\"label label-primary\"><small>m-HD</small></span>",
235
+ "k": "joVBVdm2xZWhaZWhu6vZ8cXxAl7j4qpyhNgqkwx0U/tcutx/harxdZ8BfPNcg9n1"
236
+ },
237
+ },
238
+ "mp3": {
239
+ "140": {
240
+ "size": "975.1 KB",
241
+ "f": "m4a",
242
+ "q": ".m4a",
243
+ "q_text": ".m4a (128kbps)",
244
+ "k": "joVBVdm2xZWhaZWhu6vZ8cXxAl7j4qpyhNhuxgxyU/NQ9919mbX2dYcdevRBnt0="
245
+ },
246
+ },
247
+ "related": [
248
+ {
249
+ "title": "Related Videos",
250
+ "contents": [
251
+ {
252
+ "v": "KK24ZvxLXGU",
253
+ "t": "Birthday Songs - Happy Birthday To You | 15 minutes plus"
254
+ },
255
+ ]
256
+ }
257
+ ]
258
+ }
259
+ """
260
+
261
+ def __call__(self, *args, **kwargs):
262
+ return self.main(*args, **kwargs)
263
+
264
+ def get_item(self, item_no=0):
265
+ r"""Return specific items on `self.query_one.vitems`"""
266
+ if self.video_dict:
267
+ return self.video_dict
268
+ if self.query_one.is_link:
269
+ return {"v": self.query_one.vid, "t": self.query_one.title}
270
+ all_items = self.query_one.vitems
271
+ assert (
272
+ self.item_no < len(all_items) - 1
273
+ ), "The item_no is greater than largest item's index - try lower value"
274
+
275
+ return self.query_one.vitems[item_no or self.item_no]
276
+
277
+ def get_payload(self):
278
+ return {
279
+ "hl": "en",
280
+ "k_page": "home",
281
+ "k_query": f"https://www.youtube.com/watch?v={self.get_item().get('v')}",
282
+ "q_auto": "1",
283
+ }
284
+
285
+ def __main__(self, *args, **kwargs):
286
+ return self.main(*args, **kwargs)
287
+
288
+ def __enter__(self, *args, **kwargs):
289
+ return self.__main__(*args, **kwargs)
290
+
291
+ def __exit__(self, *args, **kwargs):
292
+ self.processed = False
293
+
294
+ def main(self, item_no: int = 0, timeout: int = 30):
295
+ r"""Requests for video formats and related videos
296
+ :param item_no: (Optional) Index of query_one.vitems
297
+ :type item_no: int
298
+ :param timeout: (Optional)Http request timeout
299
+ :type timeout: int
300
+ """
301
+ self.processed = False
302
+ if item_no:
303
+ self.item_no = item_no
304
+ okay_status, resp = utils.post(
305
+ self.url, data=self.get_payload(), timeout=timeout
306
+ )
307
+
308
+ if okay_status:
309
+ dict_data = resp.json()
310
+ for key in dict_data.keys():
311
+ self.__setattr__(key, dict_data.get(key))
312
+ links = dict_data.get("links")
313
+ self.__setattr__("video", links.get("mp4"))
314
+ self.__setattr__("audio", links.get("mp3"))
315
+ self.__setattr__("related", dict_data.get("related")[0].get("contents"))
316
+ self.__setattr__("raw", dict_data)
317
+ self.processed = True
318
+
319
+ else:
320
+ logging.debug(f"{resp.headers.get('content-type')} - {resp.content}")
321
+ logging.error(f"Second query failed - [{resp.status_code} : {resp.reason}]")
322
+ return self
323
+
324
+
325
+ class third_query:
326
+ def __init__(self, query_two: object):
327
+ assert query_two.processed, "Unprocessed second_query object parsed"
328
+ self.query_two = query_two
329
+ self.url = "https://www.y2mate.com/mates/convertV2/index"
330
+ self.formats = ["mp4", "mp3"]
331
+ self.qualities_plus = ["best", "worst"]
332
+ self.qualities = {
333
+ self.formats[0]: [
334
+ "4k",
335
+ "1080p",
336
+ "720p",
337
+ "480p",
338
+ "360p",
339
+ "240p",
340
+ "144p",
341
+ "auto",
342
+ ]
343
+ + self.qualities_plus,
344
+ self.formats[1]: ["mp3", "m4a", ".m4a", "128kbps", "192kbps", "328kbps"],
345
+ }
346
+
347
+ def __call__(self, *args, **kwargs):
348
+ return self.main(*args, **kwargs)
349
+
350
+ def __enter__(self, *args, **kwargs):
351
+ return self
352
+
353
+ def __exit__(self, *args, **kwargs):
354
+ pass
355
+
356
+ def __str__(self):
357
+ return """
358
+ {
359
+ "status": "ok",
360
+ "mess": "",
361
+ "c_status": "CONVERTED",
362
+ "vid": "_z-1fTlSDF0",
363
+ "title": "Happy Birthday song",
364
+ "ftype": "mp4",
365
+ "fquality": "144p",
366
+ "dlink": "https://dl165.dlmate13.online/?file=M3R4SUNiN3JsOHJ6WWQ2a3NQS1Y5ZGlxVlZIOCtyZ01tY1VxM2xzQkNMbFlyb2t1enErekxNZElFYkZlbWQ2U1g5TkVvWGplZU55T0R4K0lvcEI3QnlHbjd0a29yU3JOOXN0eWY4UmhBbE9xdmI3bXhCZEprMHFrZU96QkpweHdQVWh0OGhRMzQyaWUzS1dTdmhEMzdsYUk0VWliZkMwWXR5OENNUENOb01rUWd6NmJQS2UxaGRZWHFDQ2c0WkpNMmZ2QTVVZmx5cWc3NVlva0Nod3NJdFpPejhmeDNhTT0%3D"
367
+ }
368
+ """
369
+
370
+ def get_payload(self, keys):
371
+ return {"k": keys.get("k"), "vid": self.query_two.vid}
372
+
373
+ def main(
374
+ self,
375
+ format: str = "mp4",
376
+ quality="auto",
377
+ resolver: str = None,
378
+ timeout: int = 30,
379
+ ):
380
+ r"""
381
+ :param format: (Optional) Media format mp4/mp3
382
+ :param quality: (Optional) Media qualiy such as 720p
383
+ :param resolver: (Optional) Additional format info : [m4a,3gp,mp4,mp3]
384
+ :param timeout: (Optional) Http requests timeout
385
+ :type type: str
386
+ :type quality: str
387
+ :type timeout: int
388
+ """
389
+ if not resolver:
390
+ resolver = "mp4" if format == "mp4" else "mp3"
391
+ if format == "mp3" and quality == "auto":
392
+ quality = "128kbps"
393
+ assert (
394
+ format in self.formats
395
+ ), f"'{format}' is not in supported formats - {self.formats}"
396
+
397
+ assert (
398
+ quality in self.qualities[format]
399
+ ), f"'{quality}' is not in supported qualities - {self.qualities[format]}"
400
+
401
+ items = self.query_two.video if format == "mp4" else self.query_two.audio
402
+ hunted = []
403
+ if quality in self.qualities_plus:
404
+ keys = list(items.keys())
405
+ if quality == self.qualities_plus[0]:
406
+ hunted.append(items[keys[0]])
407
+ else:
408
+ hunted.append(items[keys[len(keys) - 2]])
409
+ else:
410
+ for key in items.keys():
411
+ if items[key].get("q") == quality:
412
+ hunted.append(items[key])
413
+ if len(hunted) > 1:
414
+ for entry in hunted:
415
+ if entry.get("f") == resolver:
416
+ hunted.insert(0, entry)
417
+ if hunted:
418
+
419
+ def hunter_manager(souped_entry: dict = hunted[0], repeat_count=0):
420
+ payload = self.get_payload(souped_entry)
421
+ okay_status, resp = utils.post(self.url, data=payload)
422
+ if okay_status:
423
+ sanitized_feedback = resp.json()
424
+ if sanitized_feedback.get("c_status") == "CONVERTING":
425
+ if repeat_count >= 4:
426
+ return (False, {})
427
+ else:
428
+ logging.debug(
429
+ f"Converting video : sleeping for 5s - round {repeat_count+1}"
430
+ )
431
+ sleep(5)
432
+ repeat_count += 1
433
+ return hunter_manager(souped_entry)
434
+ return okay_status, resp
435
+ return okay_status, resp
436
+
437
+ okay_status, resp = hunter_manager()
438
+
439
+ if okay_status:
440
+ resp_data = hunted[0]
441
+ resp_data.update(resp.json())
442
+ return resp_data
443
+
444
+ else:
445
+ logging.debug(f"{resp.headers.get('content-type')} - {resp.content}")
446
+ logging.error(
447
+ f"Third query failed - [{resp.status_code} : {resp.reason}]"
448
+ )
449
+ return {}
450
+ else:
451
+ logging.error(
452
+ f"Zero media hunted with params : {{quality : {quality}, format : {format} }}"
453
+ )
454
+ return {}
455
+
456
+
457
+ class Handler:
458
+ def __init__(
459
+ self,
460
+ query: str,
461
+ author: str = None,
462
+ timeout: int = 30,
463
+ confirm: bool = False,
464
+ unique: bool = False,
465
+ thread: int = 0,
466
+ ):
467
+ r"""Initializes this `class`
468
+ :param query: Video name or youtube link
469
+ :type query: str
470
+ :param author: (Optional) Author (Channel) of the videos
471
+ :type author: str
472
+ :param timeout: (Optional) Http request timeout
473
+ :type timeout: int
474
+ :param confirm: (Optional) Confirm before downloading media
475
+ :type confirm: bool
476
+ :param unique: (Optional) Ignore previously downloaded media
477
+ :type confirm: bool
478
+ :param thread: (Optional) Thread the download process through `auto-save` method
479
+ :type thread int
480
+ """
481
+ self.query = query
482
+ self.author = author
483
+ self.timeout = timeout
484
+ self.keyword = None
485
+ self.confirm = confirm
486
+ self.unique = unique
487
+ self.thread = thread
488
+ self.vitems = []
489
+ self.related = []
490
+ self.dropped = []
491
+ self.total = 1
492
+ self.saved_videos = utils.get_history()
493
+
494
+ def __str__(self):
495
+ return self.query
496
+
497
+ def __enter__(self, *args, **kwargs):
498
+ return self
499
+
500
+ def __exit__(self, *args, **kwargs):
501
+ self.vitems.clear()
502
+ self.total = 1
503
+
504
+ def __call__(self, *args, **kwargs):
505
+ return self.run(*args, **kwargs)
506
+
507
+ def __filter_videos(self, entries: list) -> list:
508
+ f"""Filter videos based on keyword
509
+ :param entries: List containing dict of video id and their titles
510
+ :type entries: list
511
+ :rtype: list
512
+ """
513
+ if self.keyword:
514
+ keyword = self.keyword.lower()
515
+ resp = []
516
+ for entry in entries:
517
+ if keyword in entry.get("t").lower():
518
+ resp.append(entry)
519
+ return resp
520
+
521
+ else:
522
+ return entries
523
+
524
+ def __make_first_query(self):
525
+ r"""Sets query_one attribute to `self`"""
526
+ query_one = first_query(self.query)
527
+ self.__setattr__("query_one", query_one.main(self.timeout))
528
+ if self.query_one.is_link == False:
529
+ self.vitems.extend(self.__filter_videos(self.query_one.vitems))
530
+
531
+ @utils.error_handler(exit_on_error=True)
532
+ def __verify_item(self, second_query_obj) -> bool:
533
+ video_id = second_query_obj.vid
534
+ video_author = second_query_obj.a
535
+ video_title = second_query_obj.title
536
+ if video_id in self.saved_videos:
537
+ if self.unique:
538
+ return False, "Duplicate"
539
+ if self.confirm:
540
+ choice = confirm_from_user(
541
+ f">> Re-download : {Fore.GREEN+video_title+Fore.RESET} by {Fore.YELLOW+video_author+Fore.RESET}"
542
+ )
543
+ print("\n[*] Ok processing...", end="\r")
544
+ return choice, "User's choice"
545
+ if self.confirm:
546
+ choice = confirm_from_user(
547
+ f">> Download : {Fore.GREEN+video_title+Fore.RESET} by {Fore.YELLOW+video_author+Fore.RESET}"
548
+ )
549
+ print("\n[*] Ok processing...", end="\r")
550
+ return choice, "User's choice"
551
+ return True, "Auto"
552
+
553
+ def __make_second_query(self):
554
+ r"""Links first query with 3rd query"""
555
+ init_query_two = second_query(self.query_one)
556
+ x = 0
557
+ if not self.query_one.is_link:
558
+ for video_dict in self.vitems:
559
+ init_query_two.video_dict = video_dict
560
+ query_2 = init_query_two.main(timeout=self.timeout)
561
+ if query_2.processed:
562
+ if query_2.vid in self.dropped:
563
+ continue
564
+ if self.author and not self.author.lower() in query_2.a.lower():
565
+ logging.warning(
566
+ f"Dropping {Fore.YELLOW+query_2.title+Fore.RESET} by {Fore.RED+query_2.a+Fore.RESET}"
567
+ )
568
+ continue
569
+ else:
570
+ yes_download, reason = self.__verify_item(query_2)
571
+ if not yes_download:
572
+ logging.warning(
573
+ f"Skipping {Fore.YELLOW+query_2.title+Fore.RESET} by {Fore.MAGENTA+query_2.a+Fore.RESET} - Reason : {Fore.BLUE+reason+Fore.RESET}"
574
+ )
575
+ self.dropped.append(query_2.vid)
576
+ continue
577
+ self.related.append(query_2.related)
578
+ yield query_2
579
+ x += 1
580
+ if x >= self.total:
581
+ break
582
+ else:
583
+ logging.warning(
584
+ f"Dropping unprocessed query_two object of index {x}"
585
+ )
586
+
587
+ else:
588
+ query_2 = init_query_two.main(timeout=self.timeout)
589
+ if query_2.processed:
590
+ # self.related.extend(query_2.related)
591
+ self.vitems.extend(query_2.related)
592
+ self.query_one.is_link = False
593
+ if self.total == 1:
594
+ yield query_2
595
+ else:
596
+ for video_dict in self.vitems:
597
+ init_query_two.video_dict = video_dict
598
+ query_2 = init_query_two.main(timeout=self.timeout)
599
+ if query_2.processed:
600
+ if (
601
+ self.author
602
+ and not self.author.lower() in query_2.a.lower()
603
+ ):
604
+ logging.warning(
605
+ f"Dropping {Fore.YELLOW+query_2.title+Fore.RESET} by {Fore.RED+query_2.a+Fore.RESET}"
606
+ )
607
+ continue
608
+ else:
609
+ yes_download, reason = self.__verify_item(query_2)
610
+ if not yes_download:
611
+ logging.warning(
612
+ f"Skipping {Fore.YELLOW+query_2.title+Fore.RESET} by {Fore.MAGENTA+query_2.a+Fore.RESET} - Reason : {Fore.BLUE+reason+Fore.RESET}"
613
+ )
614
+ self.dropped.append(query_2.vid)
615
+ continue
616
+
617
+ self.related.append(query_2.related)
618
+ yield query_2
619
+ x += 1
620
+ if x >= self.total:
621
+ break
622
+ else:
623
+ logging.warning(
624
+ f"Dropping unprocessed query_two object of index {x}"
625
+ )
626
+ yield
627
+ else:
628
+ logging.warning("Dropping unprocessed query_two object")
629
+ yield
630
+
631
+ def run(
632
+ self,
633
+ format: str = "mp4",
634
+ quality: str = "auto",
635
+ resolver: str = None,
636
+ limit: int = 1,
637
+ keyword: str = None,
638
+ author: str = None,
639
+ ):
640
+ r"""Generate and yield video dictionary
641
+ :param format: (Optional) Media format mp4/mp3
642
+ :param quality: (Optional) Media qualiy such as 720p/128kbps
643
+ :param resolver: (Optional) Additional format info : [m4a,3gp,mp4,mp3]
644
+ :param limit: (Optional) Total videos to be generated
645
+ :param keyword: (Optional) Video keyword
646
+ :param author: (Optional) Author of the videos
647
+ :type quality: str
648
+ :type total: int
649
+ :type keyword: str
650
+ :type author: str
651
+ :rtype: object
652
+ """
653
+ self.author = author
654
+ self.keyword = keyword
655
+ self.total = limit
656
+ self.__make_first_query()
657
+ for query_two_obj in self.__make_second_query():
658
+ if query_two_obj:
659
+ self.vitems.extend(query_two_obj.related)
660
+ yield third_query(query_two_obj).main(
661
+ **dict(
662
+ format=format,
663
+ quality=quality,
664
+ resolver=resolver,
665
+ timeout=self.timeout,
666
+ )
667
+ )
668
+ else:
669
+ logging.error(f"Empty object - {query_two_obj}")
670
+
671
+ def generate_filename(self, third_dict: dict, naming_format: str = None) -> str:
672
+ r"""Generate filename based on the response of `third_query`
673
+ :param third_dict: response of `third_query.main()` object
674
+ :param naming_format: (Optional) Format for generating filename based on `third_dict` keys
675
+ :type third_dict: dict
676
+ :type naming_format: str
677
+ :rtype: str
678
+ """
679
+ fnm = (
680
+ f"{naming_format}" % third_dict
681
+ if naming_format
682
+ else f"{third_dict['title']} {third_dict['vid']}_{third_dict['fquality']}.{third_dict['ftype']}"
683
+ )
684
+
685
+ def sanitize(nm):
686
+ trash = [
687
+ "\\",
688
+ "/",
689
+ ":",
690
+ "*",
691
+ "?",
692
+ '"',
693
+ "<",
694
+ "|",
695
+ ">",
696
+ "y2mate.com",
697
+ "y2mate com",
698
+ ]
699
+ for val in trash:
700
+ nm = nm.replace(val, "")
701
+ return nm.strip()
702
+
703
+ return sanitize(fnm)
704
+
705
+ def auto_save(
706
+ self,
707
+ dir: str = "",
708
+ iterator: object = None,
709
+ progress_bar=True,
710
+ quiet: bool = False,
711
+ naming_format: str = None,
712
+ chunk_size: int = 512,
713
+ play: bool = False,
714
+ resume: bool = False,
715
+ *args,
716
+ **kwargs,
717
+ ):
718
+ r"""Query and save all the media
719
+ :param dir: (Optional) Path to Directory for saving the media files
720
+ :param iterator: (Optional) Function that yields third_query object - `Handler.run`
721
+ :param progress_bar: (Optional) Display progress bar
722
+ :param quiet: (Optional) Not to stdout anything
723
+ :param naming_format: (Optional) Format for generating filename
724
+ :param chunk_size: (Optional) Chunk_size for downloading files in KB
725
+ :param play: (Optional) Auto-play the media after download
726
+ :param resume: (Optional) Resume the incomplete download
727
+ :type dir: str
728
+ :type iterator: object
729
+ :type progress_bar: bool
730
+ :type quiet: bool
731
+ :type naming_format: str
732
+ :type chunk_size: int
733
+ :type play: bool
734
+ :type resume: bool
735
+ args & kwargs for the iterator
736
+ :rtype: None
737
+ """
738
+ iterator_object = iterator or self.run(*args, **kwargs)
739
+
740
+ for x, entry in enumerate(iterator_object):
741
+ if self.thread:
742
+ t1 = Thread(
743
+ target=self.save,
744
+ args=(
745
+ entry,
746
+ dir,
747
+ False,
748
+ quiet,
749
+ naming_format,
750
+ chunk_size,
751
+ play,
752
+ resume,
753
+ ),
754
+ )
755
+ t1.start()
756
+ thread_count = x + 1
757
+ if thread_count % self.thread == 0 or thread_count == self.total:
758
+ logging.debug(
759
+ f"Waiting for current running threads to finish - thread_count : {thread_count}"
760
+ )
761
+ t1.join()
762
+ else:
763
+ self.save(
764
+ entry,
765
+ dir,
766
+ progress_bar,
767
+ quiet,
768
+ naming_format,
769
+ chunk_size,
770
+ play,
771
+ resume,
772
+ )
773
+
774
+ def save(
775
+ self,
776
+ third_dict: dict,
777
+ dir: str = "",
778
+ progress_bar=True,
779
+ quiet: bool = False,
780
+ naming_format: str = None,
781
+ chunk_size: int = 512,
782
+ play: bool = False,
783
+ resume: bool = False,
784
+ disable_history=False,
785
+ ):
786
+ r"""Download media based on response of `third_query` dict-data-type
787
+ :param third_dict: Response of `third_query.run()`
788
+ :param dir: (Optional) Directory for saving the contents
789
+ :param progress_bar: (Optional) Display download progress bar
790
+ :param quiet: (Optional) Not to stdout anything
791
+ :param naming_format: (Optional) Format for generating filename
792
+ :param chunk_size: (Optional) Chunk_size for downloading files in KB
793
+ :param play: (Optional) Auto-play the media after download
794
+ :param resume: (Optional) Resume the incomplete download
795
+ :param disable_history (Optional) Don't save the download to history.
796
+ :type third_dict: dict
797
+ :type dir: str
798
+ :type progress_bar: bool
799
+ :type quiet: bool
800
+ :type naming_format: str
801
+ :type chunk_size: int
802
+ :type play: bool
803
+ :type resume: bool
804
+ :type disable_history: bool
805
+ :rtype: None
806
+ """
807
+ if third_dict:
808
+ assert third_dict.get(
809
+ "dlink"
810
+ ), "The video selected does not support that quality, try lower qualities."
811
+ if third_dict.get("mess"):
812
+ logging.warning(third_dict.get("mess"))
813
+
814
+ current_downloaded_size = 0
815
+ current_downloaded_size_in_mb = 0
816
+ filename = self.generate_filename(third_dict, naming_format)
817
+ save_to = path.join(dir, filename)
818
+ mod_headers = headers
819
+
820
+ if resume:
821
+ assert path.exists(save_to), f"File not found in path - '{save_to}'"
822
+ current_downloaded_size = path.getsize(save_to)
823
+ # Set the headers to resume download from the last byte
824
+ mod_headers = {"Range": f"bytes={current_downloaded_size}-"}
825
+ current_downloaded_size_in_mb = round(
826
+ current_downloaded_size / 1000000, 2
827
+ ) # convert to mb
828
+
829
+ resp = requests.get(third_dict["dlink"], stream=True, headers=mod_headers)
830
+
831
+ default_content_length = 0
832
+ size_in_bytes = int(
833
+ resp.headers.get("content-length", default_content_length)
834
+ )
835
+ if not size_in_bytes:
836
+ if resume:
837
+ raise FileExistsError(
838
+ f"Download completed for the file in path - '{save_to}'"
839
+ )
840
+ else:
841
+ raise Exception(
842
+ f"Cannot download file of content-length {size_in_bytes} bytes"
843
+ )
844
+
845
+ if resume:
846
+ assert (
847
+ size_in_bytes != current_downloaded_size
848
+ ), f"Download completed for the file in path - '{save_to}'"
849
+
850
+ size_in_mb = (
851
+ round(size_in_bytes / 1000000, 2) + current_downloaded_size_in_mb
852
+ )
853
+ chunk_size_in_bytes = chunk_size * 1024
854
+
855
+ third_dict["saved_to"] = (
856
+ save_to
857
+ if any([save_to.startswith("/"), ":" in save_to])
858
+ else path.join(getcwd(), dir, filename)
859
+ )
860
+ try_play_media = (
861
+ lambda: launch_media(third_dict["saved_to"]) if play else None
862
+ )
863
+ saving_mode = "ab" if resume else "wb"
864
+ if progress_bar:
865
+ if not quiet:
866
+ print(f"{filename}")
867
+ with tqdm(
868
+ total=size_in_bytes + current_downloaded_size,
869
+ bar_format="%s%d MB %s{bar} %s{l_bar}%s"
870
+ % (Fore.GREEN, size_in_mb, Fore.CYAN, Fore.YELLOW, Fore.RESET),
871
+ initial=current_downloaded_size,
872
+ ) as p_bar:
873
+ # p_bar.update(current_downloaded_size)
874
+ with open(save_to, saving_mode) as fh:
875
+ for chunks in resp.iter_content(chunk_size=chunk_size_in_bytes):
876
+ fh.write(chunks)
877
+ p_bar.update(chunk_size_in_bytes)
878
+ if not disable_history:
879
+ utils.add_history(third_dict)
880
+ try_play_media()
881
+ return save_to
882
+ else:
883
+ with open(save_to, saving_mode) as fh:
884
+ for chunks in resp.iter_content(chunk_size=chunk_size_in_bytes):
885
+ fh.write(chunks)
886
+ if not disable_history:
887
+ utils.add_history(third_dict)
888
+
889
+ try_play_media()
890
+ logging.info(f"{filename} - {size_in_mb}MB ")
891
+ return save_to
892
+ else:
893
+ logging.error(f"Empty `third_dict` parameter parsed : {third_dict}")
894
+
895
+
896
+ mp4_qualities = [
897
+ "4k",
898
+ "1080p",
899
+ "720p",
900
+ "480p",
901
+ "360p",
902
+ "240p",
903
+ "144p",
904
+ "auto",
905
+ "best",
906
+ "worst",
907
+ ]
908
+ mp3_qualities = ["mp3", "m4a", ".m4a", "128kbps", "192kbps", "328kbps"]
909
+ resolvers = ["m4a", "3gp", "mp4", "mp3"]
910
+ media_qualities = mp4_qualities + mp3_qualities
911
+
912
+ def launch_media(filepath):
913
+ """
914
+ Launch media file using default system application
915
+ """
916
+ try:
917
+ if sys.platform.startswith('darwin'): # macOS
918
+ subprocess.call(('open', filepath))
919
+ elif sys.platform.startswith('win'): # Windows
920
+ os.startfile(filepath)
921
+ elif sys.platform.startswith('linux'): # Linux
922
+ subprocess.call(('xdg-open', filepath))
923
+ except Exception as e:
924
+ print(f"Error launching media: {e}")
925
+
926
+
927
+ def confirm_from_user(message, default=False):
928
+ """
929
+ Prompt user for confirmation
930
+ """
931
+ valid = {"yes": True, "y": True, "ye": True,
932
+ "no": False, "n": False}
933
+
934
+ if default is None:
935
+ prompt = " [y/n] "
936
+ elif default:
937
+ prompt = " [Y/n] "
938
+ else:
939
+ prompt = " [y/N] "
940
+
941
+ while True:
942
+ choice = input(message + prompt).lower()
943
+ if default is not None and choice == '':
944
+ return default
945
+ elif choice in valid:
946
+ return valid[choice]
947
+ else:
948
+ print("Please respond with 'yes' or 'no' (or 'y' or 'n').")
949
+
950
+
951
+ # Create CLI app
952
+ app = CLI(name="ytdownloader", help="YouTube Video Downloader CLI")
953
+
954
+ @app.command()
955
+ @option("--author", help="Specify video author/channel")
956
+ @option("--timeout", type=int, default=30, help="HTTP request timeout")
957
+ @option("--confirm", is_flag=True, help="Confirm before downloading")
958
+ @option("--unique", is_flag=True, help="Ignore previously downloaded media")
959
+ @option("--thread", type=int, default=0, help="Thread download process")
960
+ @option("--format", default="mp4", help="Download format (mp4/mp3)")
961
+ @option("--quality", default="auto", help="Video quality")
962
+ @option("--limit", type=int, default=1, help="Total videos to download")
963
+ @option("--keyword", help="Filter videos by keyword")
964
+ @argument("query", help="Video name or YouTube link")
965
+ def download(query, author, timeout, confirm, unique, thread, format, quality, limit, keyword):
966
+ """Download YouTube videos with advanced options"""
967
+
968
+ # Create handler with parsed arguments
969
+ handler = Handler(
970
+ query=query,
971
+ author=author,
972
+ timeout=timeout,
973
+ confirm=confirm,
974
+ unique=unique,
975
+ thread=thread
976
+ )
977
+
978
+ # Run download process
979
+ handler.auto_save(
980
+ format=format,
981
+ quality=quality,
982
+ limit=limit,
983
+ keyword=keyword
984
+ )
985
+
986
+ # Replace get_args function with swiftcli's argument parsing
987
+ def main():
988
+ app.run()
989
+
990
+ if __name__ == "__main__":
1104
991
  main()