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