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