webscout 8.3.7__py3-none-any.whl → 2025.10.13__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.
- webscout/AIauto.py +250 -250
- webscout/AIbase.py +379 -379
- webscout/AIutel.py +60 -60
- webscout/Bard.py +1012 -1012
- webscout/Bing_search.py +417 -417
- webscout/DWEBS.py +529 -529
- webscout/Extra/Act.md +309 -309
- webscout/Extra/GitToolkit/__init__.py +10 -10
- webscout/Extra/GitToolkit/gitapi/README.md +110 -110
- webscout/Extra/GitToolkit/gitapi/__init__.py +11 -11
- webscout/Extra/GitToolkit/gitapi/repository.py +195 -195
- webscout/Extra/GitToolkit/gitapi/user.py +96 -96
- webscout/Extra/GitToolkit/gitapi/utils.py +61 -61
- webscout/Extra/YTToolkit/README.md +375 -375
- webscout/Extra/YTToolkit/YTdownloader.py +956 -956
- webscout/Extra/YTToolkit/__init__.py +2 -2
- webscout/Extra/YTToolkit/transcriber.py +475 -475
- webscout/Extra/YTToolkit/ytapi/README.md +44 -44
- webscout/Extra/YTToolkit/ytapi/__init__.py +6 -6
- webscout/Extra/YTToolkit/ytapi/channel.py +307 -307
- webscout/Extra/YTToolkit/ytapi/errors.py +13 -13
- webscout/Extra/YTToolkit/ytapi/extras.py +118 -118
- webscout/Extra/YTToolkit/ytapi/https.py +88 -88
- webscout/Extra/YTToolkit/ytapi/patterns.py +61 -61
- webscout/Extra/YTToolkit/ytapi/playlist.py +58 -58
- webscout/Extra/YTToolkit/ytapi/pool.py +7 -7
- webscout/Extra/YTToolkit/ytapi/query.py +39 -39
- webscout/Extra/YTToolkit/ytapi/stream.py +62 -62
- webscout/Extra/YTToolkit/ytapi/utils.py +62 -62
- webscout/Extra/YTToolkit/ytapi/video.py +232 -232
- webscout/Extra/autocoder/__init__.py +9 -9
- webscout/Extra/autocoder/autocoder.py +1105 -1105
- webscout/Extra/autocoder/autocoder_utiles.py +332 -332
- webscout/Extra/gguf.md +429 -429
- webscout/Extra/gguf.py +1213 -1213
- webscout/Extra/tempmail/README.md +487 -487
- webscout/Extra/tempmail/__init__.py +27 -27
- webscout/Extra/tempmail/async_utils.py +140 -140
- webscout/Extra/tempmail/base.py +160 -160
- webscout/Extra/tempmail/cli.py +186 -186
- webscout/Extra/tempmail/emailnator.py +84 -84
- webscout/Extra/tempmail/mail_tm.py +360 -360
- webscout/Extra/tempmail/temp_mail_io.py +291 -291
- webscout/Extra/weather.md +281 -281
- webscout/Extra/weather.py +193 -193
- webscout/Litlogger/README.md +10 -10
- webscout/Litlogger/__init__.py +15 -15
- webscout/Litlogger/formats.py +13 -13
- webscout/Litlogger/handlers.py +121 -121
- webscout/Litlogger/levels.py +13 -13
- webscout/Litlogger/logger.py +134 -134
- webscout/Provider/AISEARCH/Perplexity.py +332 -332
- webscout/Provider/AISEARCH/README.md +279 -279
- webscout/Provider/AISEARCH/__init__.py +16 -1
- webscout/Provider/AISEARCH/felo_search.py +206 -206
- webscout/Provider/AISEARCH/genspark_search.py +323 -323
- webscout/Provider/AISEARCH/hika_search.py +185 -185
- webscout/Provider/AISEARCH/iask_search.py +410 -410
- webscout/Provider/AISEARCH/monica_search.py +219 -219
- webscout/Provider/AISEARCH/scira_search.py +316 -316
- webscout/Provider/AISEARCH/stellar_search.py +177 -177
- webscout/Provider/AISEARCH/webpilotai_search.py +255 -255
- webscout/Provider/Aitopia.py +314 -314
- webscout/Provider/Andi.py +1 -1
- webscout/Provider/Apriel.py +306 -0
- webscout/Provider/ChatGPTClone.py +237 -236
- webscout/Provider/ChatSandbox.py +343 -343
- webscout/Provider/Cloudflare.py +324 -324
- webscout/Provider/Cohere.py +208 -208
- webscout/Provider/Deepinfra.py +370 -366
- webscout/Provider/ExaAI.py +260 -260
- webscout/Provider/ExaChat.py +308 -308
- webscout/Provider/Flowith.py +221 -221
- webscout/Provider/GMI.py +293 -0
- webscout/Provider/Gemini.py +164 -164
- webscout/Provider/GeminiProxy.py +167 -167
- webscout/Provider/GithubChat.py +371 -372
- webscout/Provider/Groq.py +800 -800
- webscout/Provider/HeckAI.py +383 -383
- webscout/Provider/Jadve.py +282 -282
- webscout/Provider/K2Think.py +307 -307
- webscout/Provider/Koboldai.py +205 -205
- webscout/Provider/LambdaChat.py +423 -423
- webscout/Provider/Nemotron.py +244 -244
- webscout/Provider/Netwrck.py +248 -248
- webscout/Provider/OLLAMA.py +395 -395
- webscout/Provider/OPENAI/Cloudflare.py +393 -393
- webscout/Provider/OPENAI/FalconH1.py +451 -451
- webscout/Provider/OPENAI/FreeGemini.py +296 -296
- webscout/Provider/OPENAI/K2Think.py +431 -431
- webscout/Provider/OPENAI/NEMOTRON.py +240 -240
- webscout/Provider/OPENAI/PI.py +427 -427
- webscout/Provider/OPENAI/README.md +959 -959
- webscout/Provider/OPENAI/TogetherAI.py +345 -345
- webscout/Provider/OPENAI/TwoAI.py +465 -465
- webscout/Provider/OPENAI/__init__.py +33 -18
- webscout/Provider/OPENAI/base.py +248 -248
- webscout/Provider/OPENAI/chatglm.py +528 -0
- webscout/Provider/OPENAI/chatgpt.py +592 -592
- webscout/Provider/OPENAI/chatgptclone.py +521 -521
- webscout/Provider/OPENAI/chatsandbox.py +202 -202
- webscout/Provider/OPENAI/deepinfra.py +318 -314
- webscout/Provider/OPENAI/e2b.py +1665 -1665
- webscout/Provider/OPENAI/exaai.py +420 -420
- webscout/Provider/OPENAI/exachat.py +452 -452
- webscout/Provider/OPENAI/friendli.py +232 -232
- webscout/Provider/OPENAI/{refact.py → gmi.py} +324 -274
- webscout/Provider/OPENAI/groq.py +364 -364
- webscout/Provider/OPENAI/heckai.py +314 -314
- webscout/Provider/OPENAI/llmchatco.py +337 -337
- webscout/Provider/OPENAI/netwrck.py +355 -355
- webscout/Provider/OPENAI/oivscode.py +290 -290
- webscout/Provider/OPENAI/opkfc.py +518 -518
- webscout/Provider/OPENAI/pydantic_imports.py +1 -1
- webscout/Provider/OPENAI/scirachat.py +535 -535
- webscout/Provider/OPENAI/sonus.py +308 -308
- webscout/Provider/OPENAI/standardinput.py +442 -442
- webscout/Provider/OPENAI/textpollinations.py +340 -340
- webscout/Provider/OPENAI/toolbaz.py +419 -416
- webscout/Provider/OPENAI/typefully.py +362 -362
- webscout/Provider/OPENAI/utils.py +295 -295
- webscout/Provider/OPENAI/venice.py +436 -436
- webscout/Provider/OPENAI/wisecat.py +387 -387
- webscout/Provider/OPENAI/writecream.py +166 -166
- webscout/Provider/OPENAI/x0gpt.py +378 -378
- webscout/Provider/OPENAI/yep.py +389 -389
- webscout/Provider/OpenGPT.py +230 -230
- webscout/Provider/Openai.py +243 -243
- webscout/Provider/PI.py +405 -405
- webscout/Provider/Perplexitylabs.py +430 -430
- webscout/Provider/QwenLM.py +272 -272
- webscout/Provider/STT/__init__.py +16 -1
- webscout/Provider/Sambanova.py +257 -257
- webscout/Provider/StandardInput.py +309 -309
- webscout/Provider/TTI/README.md +82 -82
- webscout/Provider/TTI/__init__.py +33 -18
- webscout/Provider/TTI/aiarta.py +413 -413
- webscout/Provider/TTI/base.py +136 -136
- webscout/Provider/TTI/bing.py +243 -243
- webscout/Provider/TTI/gpt1image.py +149 -149
- webscout/Provider/TTI/imagen.py +196 -196
- webscout/Provider/TTI/infip.py +211 -211
- webscout/Provider/TTI/magicstudio.py +232 -232
- webscout/Provider/TTI/monochat.py +219 -219
- webscout/Provider/TTI/piclumen.py +214 -214
- webscout/Provider/TTI/pixelmuse.py +232 -232
- webscout/Provider/TTI/pollinations.py +232 -232
- webscout/Provider/TTI/together.py +288 -288
- webscout/Provider/TTI/utils.py +12 -12
- webscout/Provider/TTI/venice.py +367 -367
- webscout/Provider/TTS/README.md +192 -192
- webscout/Provider/TTS/__init__.py +33 -18
- webscout/Provider/TTS/parler.py +110 -110
- webscout/Provider/TTS/streamElements.py +333 -333
- webscout/Provider/TTS/utils.py +280 -280
- webscout/Provider/TeachAnything.py +237 -237
- webscout/Provider/TextPollinationsAI.py +310 -310
- webscout/Provider/TogetherAI.py +356 -356
- webscout/Provider/TwoAI.py +312 -312
- webscout/Provider/TypliAI.py +311 -311
- webscout/Provider/UNFINISHED/ChatHub.py +208 -208
- webscout/Provider/UNFINISHED/ChutesAI.py +313 -313
- webscout/Provider/UNFINISHED/GizAI.py +294 -294
- webscout/Provider/UNFINISHED/Marcus.py +198 -198
- webscout/Provider/UNFINISHED/Qodo.py +477 -477
- webscout/Provider/UNFINISHED/VercelAIGateway.py +338 -338
- webscout/Provider/UNFINISHED/XenAI.py +324 -324
- webscout/Provider/UNFINISHED/Youchat.py +330 -330
- webscout/Provider/UNFINISHED/liner.py +334 -0
- webscout/Provider/UNFINISHED/liner_api_request.py +262 -262
- webscout/Provider/UNFINISHED/puterjs.py +634 -634
- webscout/Provider/UNFINISHED/samurai.py +223 -223
- webscout/Provider/UNFINISHED/test_lmarena.py +119 -119
- webscout/Provider/Venice.py +250 -250
- webscout/Provider/VercelAI.py +256 -256
- webscout/Provider/WiseCat.py +231 -231
- webscout/Provider/WrDoChat.py +366 -366
- webscout/Provider/__init__.py +33 -18
- webscout/Provider/ai4chat.py +174 -174
- webscout/Provider/akashgpt.py +331 -331
- webscout/Provider/cerebras.py +446 -446
- webscout/Provider/chatglm.py +394 -301
- webscout/Provider/cleeai.py +211 -211
- webscout/Provider/elmo.py +282 -282
- webscout/Provider/geminiapi.py +208 -208
- webscout/Provider/granite.py +261 -261
- webscout/Provider/hermes.py +263 -263
- webscout/Provider/julius.py +223 -223
- webscout/Provider/learnfastai.py +309 -309
- webscout/Provider/llama3mitril.py +214 -214
- webscout/Provider/llmchat.py +243 -243
- webscout/Provider/llmchatco.py +290 -290
- webscout/Provider/meta.py +801 -801
- webscout/Provider/oivscode.py +309 -309
- webscout/Provider/scira_chat.py +383 -383
- webscout/Provider/searchchat.py +292 -292
- webscout/Provider/sonus.py +258 -258
- webscout/Provider/toolbaz.py +370 -367
- webscout/Provider/turboseek.py +273 -273
- webscout/Provider/typefully.py +207 -207
- webscout/Provider/yep.py +372 -372
- webscout/__init__.py +27 -31
- webscout/__main__.py +5 -5
- webscout/auth/api_key_manager.py +189 -189
- webscout/auth/config.py +175 -175
- webscout/auth/models.py +185 -185
- webscout/auth/routes.py +663 -664
- webscout/auth/simple_logger.py +236 -236
- webscout/cli.py +523 -523
- webscout/conversation.py +438 -438
- webscout/exceptions.py +361 -361
- webscout/litagent/Readme.md +298 -298
- webscout/litagent/__init__.py +28 -28
- webscout/litagent/agent.py +581 -581
- webscout/litagent/constants.py +59 -59
- webscout/litprinter/__init__.py +58 -58
- webscout/models.py +181 -181
- webscout/optimizers.py +419 -419
- webscout/prompt_manager.py +288 -288
- webscout/sanitize.py +1078 -1078
- webscout/scout/README.md +401 -401
- webscout/scout/__init__.py +8 -8
- webscout/scout/core/__init__.py +6 -6
- webscout/scout/core/crawler.py +297 -297
- webscout/scout/core/scout.py +706 -706
- webscout/scout/core/search_result.py +95 -95
- webscout/scout/core/text_analyzer.py +62 -62
- webscout/scout/core/text_utils.py +277 -277
- webscout/scout/core/web_analyzer.py +51 -51
- webscout/scout/element.py +599 -599
- webscout/scout/parsers/__init__.py +69 -69
- webscout/scout/parsers/html5lib_parser.py +172 -172
- webscout/scout/parsers/html_parser.py +236 -236
- webscout/scout/parsers/lxml_parser.py +178 -178
- webscout/scout/utils.py +37 -37
- webscout/search/__init__.py +51 -0
- webscout/search/base.py +195 -0
- webscout/search/duckduckgo_main.py +54 -0
- webscout/search/engines/__init__.py +48 -0
- webscout/search/engines/bing.py +84 -0
- webscout/search/engines/bing_news.py +52 -0
- webscout/search/engines/brave.py +43 -0
- webscout/search/engines/duckduckgo/__init__.py +25 -0
- webscout/search/engines/duckduckgo/answers.py +78 -0
- webscout/search/engines/duckduckgo/base.py +187 -0
- webscout/search/engines/duckduckgo/images.py +97 -0
- webscout/search/engines/duckduckgo/maps.py +168 -0
- webscout/search/engines/duckduckgo/news.py +68 -0
- webscout/search/engines/duckduckgo/suggestions.py +21 -0
- webscout/search/engines/duckduckgo/text.py +211 -0
- webscout/search/engines/duckduckgo/translate.py +47 -0
- webscout/search/engines/duckduckgo/videos.py +63 -0
- webscout/search/engines/duckduckgo/weather.py +74 -0
- webscout/search/engines/mojeek.py +37 -0
- webscout/search/engines/wikipedia.py +56 -0
- webscout/search/engines/yahoo.py +65 -0
- webscout/search/engines/yahoo_news.py +64 -0
- webscout/search/engines/yandex.py +43 -0
- webscout/search/engines/yep/__init__.py +13 -0
- webscout/search/engines/yep/base.py +32 -0
- webscout/search/engines/yep/images.py +99 -0
- webscout/search/engines/yep/suggestions.py +35 -0
- webscout/search/engines/yep/text.py +114 -0
- webscout/search/http_client.py +156 -0
- webscout/search/results.py +137 -0
- webscout/search/yep_main.py +44 -0
- webscout/swiftcli/Readme.md +323 -323
- webscout/swiftcli/__init__.py +95 -95
- webscout/swiftcli/core/__init__.py +7 -7
- webscout/swiftcli/core/cli.py +308 -308
- webscout/swiftcli/core/context.py +104 -104
- webscout/swiftcli/core/group.py +241 -241
- webscout/swiftcli/decorators/__init__.py +28 -28
- webscout/swiftcli/decorators/command.py +221 -221
- webscout/swiftcli/decorators/options.py +220 -220
- webscout/swiftcli/decorators/output.py +302 -302
- webscout/swiftcli/exceptions.py +21 -21
- webscout/swiftcli/plugins/__init__.py +9 -9
- webscout/swiftcli/plugins/base.py +135 -135
- webscout/swiftcli/plugins/manager.py +269 -269
- webscout/swiftcli/utils/__init__.py +59 -59
- webscout/swiftcli/utils/formatting.py +252 -252
- webscout/swiftcli/utils/parsing.py +267 -267
- webscout/update_checker.py +117 -117
- webscout/version.py +1 -1
- webscout/version.py.bak +2 -0
- webscout/zeroart/README.md +89 -89
- webscout/zeroart/__init__.py +134 -134
- webscout/zeroart/base.py +66 -66
- webscout/zeroart/effects.py +100 -100
- webscout/zeroart/fonts.py +1238 -1238
- {webscout-8.3.7.dist-info → webscout-2025.10.13.dist-info}/METADATA +936 -937
- webscout-2025.10.13.dist-info/RECORD +329 -0
- webscout/Provider/AISEARCH/DeepFind.py +0 -254
- webscout/Provider/OPENAI/Qwen3.py +0 -303
- webscout/Provider/OPENAI/qodo.py +0 -630
- webscout/Provider/OPENAI/xenai.py +0 -514
- webscout/tempid.py +0 -134
- webscout/webscout_search.py +0 -1183
- webscout/webscout_search_async.py +0 -649
- webscout/yep_search.py +0 -346
- webscout-8.3.7.dist-info/RECORD +0 -301
- {webscout-8.3.7.dist-info → webscout-2025.10.13.dist-info}/WHEEL +0 -0
- {webscout-8.3.7.dist-info → webscout-2025.10.13.dist-info}/entry_points.txt +0 -0
- {webscout-8.3.7.dist-info → webscout-2025.10.13.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.3.7.dist-info → webscout-2025.10.13.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from ....exceptions import WebscoutE
|
|
6
|
+
from .base import DuckDuckGoBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DuckDuckGoMaps(DuckDuckGoBase):
|
|
10
|
+
def run(self, *args, **kwargs) -> list[dict[str, str]]:
|
|
11
|
+
keywords = args[0] if args else kwargs.get("keywords")
|
|
12
|
+
place = args[1] if len(args) > 1 else kwargs.get("place")
|
|
13
|
+
street = args[2] if len(args) > 2 else kwargs.get("street")
|
|
14
|
+
city = args[3] if len(args) > 3 else kwargs.get("city")
|
|
15
|
+
county = args[4] if len(args) > 4 else kwargs.get("county")
|
|
16
|
+
state = args[5] if len(args) > 5 else kwargs.get("state")
|
|
17
|
+
country = args[6] if len(args) > 6 else kwargs.get("country")
|
|
18
|
+
postalcode = args[7] if len(args) > 7 else kwargs.get("postalcode")
|
|
19
|
+
latitude = args[8] if len(args) > 8 else kwargs.get("latitude")
|
|
20
|
+
longitude = args[9] if len(args) > 9 else kwargs.get("longitude")
|
|
21
|
+
radius = args[10] if len(args) > 10 else kwargs.get("radius", 0)
|
|
22
|
+
max_results = args[11] if len(args) > 11 else kwargs.get("max_results")
|
|
23
|
+
|
|
24
|
+
assert keywords, "keywords is mandatory"
|
|
25
|
+
|
|
26
|
+
vqd = self._get_vqd(keywords)
|
|
27
|
+
|
|
28
|
+
# if longitude and latitude are specified, skip the request about bbox to the nominatim api
|
|
29
|
+
if latitude and longitude:
|
|
30
|
+
lat_t = Decimal(latitude.replace(",", "."))
|
|
31
|
+
lat_b = Decimal(latitude.replace(",", "."))
|
|
32
|
+
lon_l = Decimal(longitude.replace(",", "."))
|
|
33
|
+
lon_r = Decimal(longitude.replace(",", "."))
|
|
34
|
+
if radius == 0:
|
|
35
|
+
radius = 1
|
|
36
|
+
# otherwise request about bbox to nominatim api
|
|
37
|
+
else:
|
|
38
|
+
if place:
|
|
39
|
+
params = {
|
|
40
|
+
"q": place,
|
|
41
|
+
"polygon_geojson": "0",
|
|
42
|
+
"format": "jsonv2",
|
|
43
|
+
}
|
|
44
|
+
else:
|
|
45
|
+
params = {
|
|
46
|
+
"polygon_geojson": "0",
|
|
47
|
+
"format": "jsonv2",
|
|
48
|
+
}
|
|
49
|
+
if street:
|
|
50
|
+
params["street"] = street
|
|
51
|
+
if city:
|
|
52
|
+
params["city"] = city
|
|
53
|
+
if county:
|
|
54
|
+
params["county"] = county
|
|
55
|
+
if state:
|
|
56
|
+
params["state"] = state
|
|
57
|
+
if country:
|
|
58
|
+
params["country"] = country
|
|
59
|
+
if postalcode:
|
|
60
|
+
params["postalcode"] = postalcode
|
|
61
|
+
# request nominatim api to get coordinates box
|
|
62
|
+
resp_content = self._get_url(
|
|
63
|
+
"GET",
|
|
64
|
+
"https://nominatim.openstreetmap.org/search.php",
|
|
65
|
+
params=params,
|
|
66
|
+
).content
|
|
67
|
+
if resp_content == b"[]":
|
|
68
|
+
raise WebscoutE("maps() Coordinates are not found, check function parameters.")
|
|
69
|
+
resp_json = self.json_loads(resp_content)
|
|
70
|
+
coordinates = resp_json[0]["boundingbox"]
|
|
71
|
+
lat_t, lon_l = Decimal(coordinates[1]), Decimal(coordinates[2])
|
|
72
|
+
lat_b, lon_r = Decimal(coordinates[0]), Decimal(coordinates[3])
|
|
73
|
+
|
|
74
|
+
# if a radius is specified, expand the search square
|
|
75
|
+
lat_t += Decimal(radius) * Decimal(0.008983)
|
|
76
|
+
lat_b -= Decimal(radius) * Decimal(0.008983)
|
|
77
|
+
lon_l -= Decimal(radius) * Decimal(0.008983)
|
|
78
|
+
lon_r += Decimal(radius) * Decimal(0.008983)
|
|
79
|
+
|
|
80
|
+
cache = set()
|
|
81
|
+
results: list[dict[str, str]] = []
|
|
82
|
+
|
|
83
|
+
def _maps_page(
|
|
84
|
+
bbox: tuple[Decimal, Decimal, Decimal, Decimal],
|
|
85
|
+
) -> list[dict[str, str]] | None:
|
|
86
|
+
if max_results and len(results) >= max_results:
|
|
87
|
+
return None
|
|
88
|
+
lat_t, lon_l, lat_b, lon_r = bbox
|
|
89
|
+
params = {
|
|
90
|
+
"q": keywords,
|
|
91
|
+
"vqd": vqd,
|
|
92
|
+
"tg": "maps_places",
|
|
93
|
+
"rt": "D",
|
|
94
|
+
"mkexp": "b",
|
|
95
|
+
"wiki_info": "1",
|
|
96
|
+
"is_requery": "1",
|
|
97
|
+
"bbox_tl": f"{lat_t},{lon_l}",
|
|
98
|
+
"bbox_br": f"{lat_b},{lon_r}",
|
|
99
|
+
"strict_bbox": "1",
|
|
100
|
+
}
|
|
101
|
+
resp_content = self._get_url("GET", "https://duckduckgo.com/local.js", params=params).content
|
|
102
|
+
resp_json = self.json_loads(resp_content)
|
|
103
|
+
page_data = resp_json.get("results", [])
|
|
104
|
+
|
|
105
|
+
page_results = []
|
|
106
|
+
for res in page_data:
|
|
107
|
+
r_name = f'{res["name"]} {res["address"]}'
|
|
108
|
+
if r_name in cache:
|
|
109
|
+
continue
|
|
110
|
+
else:
|
|
111
|
+
cache.add(r_name)
|
|
112
|
+
result = {
|
|
113
|
+
"title": res["name"],
|
|
114
|
+
"address": res["address"],
|
|
115
|
+
"country_code": res["country_code"],
|
|
116
|
+
"url": self._normalize_url(res["website"]),
|
|
117
|
+
"phone": res["phone"] or "",
|
|
118
|
+
"latitude": res["coordinates"]["latitude"],
|
|
119
|
+
"longitude": res["coordinates"]["longitude"],
|
|
120
|
+
"source": self._normalize_url(res["url"]),
|
|
121
|
+
"image": x.get("image", "") if (x := res["embed"]) else "",
|
|
122
|
+
"desc": x.get("description", "") if (x := res["embed"]) else "",
|
|
123
|
+
"hours": res["hours"] or "",
|
|
124
|
+
"category": res["ddg_category"] or "",
|
|
125
|
+
"facebook": f"www.facebook.com/profile.php?id={x}" if (x := res["facebook_id"]) else "",
|
|
126
|
+
"instagram": f"https://www.instagram.com/{x}" if (x := res["instagram_id"]) else "",
|
|
127
|
+
"twitter": f"https://twitter.com/{x}" if (x := res["twitter_id"]) else "",
|
|
128
|
+
}
|
|
129
|
+
page_results.append(result)
|
|
130
|
+
return page_results
|
|
131
|
+
|
|
132
|
+
start_bbox = (lat_t, lon_l, lat_b, lon_r)
|
|
133
|
+
work_bboxes = [start_bbox]
|
|
134
|
+
while work_bboxes:
|
|
135
|
+
queue_bboxes = []
|
|
136
|
+
tasks = []
|
|
137
|
+
for bbox in work_bboxes:
|
|
138
|
+
tasks.append(bbox)
|
|
139
|
+
if self._calculate_distance(lat_t, lon_l, lat_b, lon_r) > 1:
|
|
140
|
+
lat_t, lon_l, lat_b, lon_r = bbox
|
|
141
|
+
lat_middle = (lat_t + lat_b) / 2
|
|
142
|
+
lon_middle = (lon_l + lon_r) / 2
|
|
143
|
+
bbox1 = (lat_t, lon_l, lat_middle, lon_middle)
|
|
144
|
+
bbox2 = (lat_t, lon_middle, lat_middle, lon_r)
|
|
145
|
+
bbox3 = (lat_middle, lon_l, lat_b, lon_middle)
|
|
146
|
+
bbox4 = (lat_middle, lon_middle, lat_b, lon_r)
|
|
147
|
+
queue_bboxes.extend([bbox1, bbox2, bbox3, bbox4])
|
|
148
|
+
|
|
149
|
+
work_bboxes_results = []
|
|
150
|
+
try:
|
|
151
|
+
for r in self._executor.map(_maps_page, tasks):
|
|
152
|
+
if r:
|
|
153
|
+
work_bboxes_results.extend(r)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
raise e
|
|
156
|
+
|
|
157
|
+
for x in work_bboxes_results:
|
|
158
|
+
if isinstance(x, list):
|
|
159
|
+
results.extend(x)
|
|
160
|
+
elif isinstance(x, dict):
|
|
161
|
+
results.append(x)
|
|
162
|
+
|
|
163
|
+
work_bboxes = queue_bboxes
|
|
164
|
+
if not max_results or len(results) >= max_results or len(work_bboxes_results) == 0:
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
return list(self.islice(results, max_results))
|
|
168
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
from ....exceptions import WebscoutE
|
|
6
|
+
from .base import DuckDuckGoBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DuckDuckGoNews(DuckDuckGoBase):
|
|
10
|
+
def run(self, *args, **kwargs) -> list[dict[str, str]]:
|
|
11
|
+
keywords = args[0] if args else kwargs.get("keywords")
|
|
12
|
+
region = args[1] if len(args) > 1 else kwargs.get("region", "wt-wt")
|
|
13
|
+
safesearch = args[2] if len(args) > 2 else kwargs.get("safesearch", "moderate")
|
|
14
|
+
timelimit = args[3] if len(args) > 3 else kwargs.get("timelimit")
|
|
15
|
+
max_results = args[4] if len(args) > 4 else kwargs.get("max_results")
|
|
16
|
+
|
|
17
|
+
assert keywords, "keywords is mandatory"
|
|
18
|
+
|
|
19
|
+
vqd = self._get_vqd(keywords)
|
|
20
|
+
|
|
21
|
+
safesearch_base = {"on": "1", "moderate": "-1", "off": "-2"}
|
|
22
|
+
payload = {
|
|
23
|
+
"l": region,
|
|
24
|
+
"o": "json",
|
|
25
|
+
"noamp": "1",
|
|
26
|
+
"q": keywords,
|
|
27
|
+
"vqd": vqd,
|
|
28
|
+
"p": safesearch_base[safesearch.lower()],
|
|
29
|
+
}
|
|
30
|
+
if timelimit:
|
|
31
|
+
payload["df"] = timelimit
|
|
32
|
+
|
|
33
|
+
cache = set()
|
|
34
|
+
results: list[dict[str, str]] = []
|
|
35
|
+
|
|
36
|
+
def _news_page(s: int) -> list[dict[str, str]]:
|
|
37
|
+
payload["s"] = f"{s}"
|
|
38
|
+
resp_content = self._get_url("GET", "https://duckduckgo.com/news.js", params=payload).content
|
|
39
|
+
resp_json = self.json_loads(resp_content)
|
|
40
|
+
page_data = resp_json.get("results", [])
|
|
41
|
+
page_results = []
|
|
42
|
+
for row in page_data:
|
|
43
|
+
if row["url"] not in cache:
|
|
44
|
+
cache.add(row["url"])
|
|
45
|
+
image_url = row.get("image", None)
|
|
46
|
+
result = {
|
|
47
|
+
"date": datetime.fromtimestamp(row["date"], timezone.utc).isoformat(),
|
|
48
|
+
"title": row["title"],
|
|
49
|
+
"body": self._normalize(row["excerpt"]),
|
|
50
|
+
"url": self._normalize_url(row["url"]),
|
|
51
|
+
"image": self._normalize_url(image_url),
|
|
52
|
+
"source": row["source"],
|
|
53
|
+
}
|
|
54
|
+
page_results.append(result)
|
|
55
|
+
return page_results
|
|
56
|
+
|
|
57
|
+
slist = [0]
|
|
58
|
+
if max_results:
|
|
59
|
+
max_results = min(max_results, 120)
|
|
60
|
+
slist.extend(range(30, max_results, 30))
|
|
61
|
+
try:
|
|
62
|
+
for r in self._executor.map(_news_page, slist):
|
|
63
|
+
results.extend(r)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
raise e
|
|
66
|
+
|
|
67
|
+
return list(self.islice(results, max_results))
|
|
68
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ....exceptions import WebscoutE
|
|
4
|
+
from .base import DuckDuckGoBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DuckDuckGoSuggestions(DuckDuckGoBase):
|
|
8
|
+
def run(self, *args, **kwargs) -> list[dict[str, str]]:
|
|
9
|
+
keywords = args[0] if args else kwargs.get("keywords")
|
|
10
|
+
region = args[1] if len(args) > 1 else kwargs.get("region", "wt-wt")
|
|
11
|
+
|
|
12
|
+
assert keywords, "keywords is mandatory"
|
|
13
|
+
|
|
14
|
+
payload = {
|
|
15
|
+
"q": keywords,
|
|
16
|
+
"kl": region,
|
|
17
|
+
}
|
|
18
|
+
resp_content = self._get_url("GET", "https://duckduckgo.com/ac/", params=payload).content
|
|
19
|
+
page_data = self.json_loads(resp_content)
|
|
20
|
+
return [r for r in page_data]
|
|
21
|
+
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""DuckDuckGo text search."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from random import shuffle
|
|
7
|
+
|
|
8
|
+
from ....exceptions import WebscoutE
|
|
9
|
+
from .base import DuckDuckGoBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DuckDuckGoTextSearch(DuckDuckGoBase):
|
|
13
|
+
"""DuckDuckGo text/web search."""
|
|
14
|
+
|
|
15
|
+
def run(self, *args, **kwargs) -> list[dict[str, str]]:
|
|
16
|
+
"""Perform text search on DuckDuckGo.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
keywords: Search query.
|
|
20
|
+
region: Region code (e.g., wt-wt, us-en).
|
|
21
|
+
safesearch: on, moderate, or off.
|
|
22
|
+
timelimit: d, w, m, or y.
|
|
23
|
+
backend: html, lite, or auto.
|
|
24
|
+
max_results: Maximum number of results.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List of search result dictionaries.
|
|
28
|
+
"""
|
|
29
|
+
keywords = args[0] if args else kwargs.get("keywords")
|
|
30
|
+
region = args[1] if len(args) > 1 else kwargs.get("region", "wt-wt")
|
|
31
|
+
safesearch = args[2] if len(args) > 2 else kwargs.get("safesearch", "moderate")
|
|
32
|
+
timelimit = args[3] if len(args) > 3 else kwargs.get("timelimit")
|
|
33
|
+
backend = args[4] if len(args) > 4 else kwargs.get("backend", "auto")
|
|
34
|
+
max_results = args[5] if len(args) > 5 else kwargs.get("max_results")
|
|
35
|
+
|
|
36
|
+
if backend in ("api", "ecosia"):
|
|
37
|
+
warnings.warn(f"{backend=} is deprecated, using backend='auto'", stacklevel=2)
|
|
38
|
+
backend = "auto"
|
|
39
|
+
backends = ["html", "lite"] if backend == "auto" else [backend]
|
|
40
|
+
shuffle(backends)
|
|
41
|
+
|
|
42
|
+
results, err = [], None
|
|
43
|
+
for b in backends:
|
|
44
|
+
try:
|
|
45
|
+
if b == "html":
|
|
46
|
+
results = self._text_html(keywords, region, timelimit, max_results)
|
|
47
|
+
elif b == "lite":
|
|
48
|
+
results = self._text_lite(keywords, region, timelimit, max_results)
|
|
49
|
+
return results
|
|
50
|
+
except Exception as ex:
|
|
51
|
+
err = ex
|
|
52
|
+
|
|
53
|
+
raise WebscoutE(err)
|
|
54
|
+
|
|
55
|
+
def _text_html(
|
|
56
|
+
self,
|
|
57
|
+
keywords: str,
|
|
58
|
+
region: str = "wt-wt",
|
|
59
|
+
timelimit: str | None = None,
|
|
60
|
+
max_results: int | None = None,
|
|
61
|
+
) -> list[dict[str, str]]:
|
|
62
|
+
"""Text search using HTML backend."""
|
|
63
|
+
assert keywords, "keywords is mandatory"
|
|
64
|
+
|
|
65
|
+
payload = {
|
|
66
|
+
"q": keywords,
|
|
67
|
+
"s": "0",
|
|
68
|
+
"o": "json",
|
|
69
|
+
"api": "d.js",
|
|
70
|
+
"vqd": "",
|
|
71
|
+
"kl": region,
|
|
72
|
+
"bing_market": region,
|
|
73
|
+
}
|
|
74
|
+
if timelimit:
|
|
75
|
+
payload["df"] = timelimit
|
|
76
|
+
if max_results and max_results > 20:
|
|
77
|
+
vqd = self._get_vqd(keywords)
|
|
78
|
+
payload["vqd"] = vqd
|
|
79
|
+
|
|
80
|
+
cache = set()
|
|
81
|
+
results: list[dict[str, str]] = []
|
|
82
|
+
|
|
83
|
+
def _text_html_page(s: int) -> list[dict[str, str]]:
|
|
84
|
+
payload["s"] = f"{s}"
|
|
85
|
+
resp_content = self._get_url("POST", "https://html.duckduckgo.com/html", data=payload).content
|
|
86
|
+
if b"No results." in resp_content:
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
page_results = []
|
|
90
|
+
tree = self.parser.fromstring(resp_content)
|
|
91
|
+
elements = tree.xpath("//div[h2]")
|
|
92
|
+
if not isinstance(elements, list):
|
|
93
|
+
return []
|
|
94
|
+
for e in elements:
|
|
95
|
+
if isinstance(e, self.parser.etree.Element):
|
|
96
|
+
hrefxpath = e.xpath("./a/@href")
|
|
97
|
+
href = str(hrefxpath[0]) if hrefxpath and isinstance(hrefxpath, list) else None
|
|
98
|
+
if (
|
|
99
|
+
href
|
|
100
|
+
and href not in cache
|
|
101
|
+
and not href.startswith(
|
|
102
|
+
("http://www.google.com/search?q=", "https://duckduckgo.com/y.js?ad_domain")
|
|
103
|
+
)
|
|
104
|
+
):
|
|
105
|
+
cache.add(href)
|
|
106
|
+
titlexpath = e.xpath("./h2/a/text()")
|
|
107
|
+
title = str(titlexpath[0]) if titlexpath and isinstance(titlexpath, list) else ""
|
|
108
|
+
bodyxpath = e.xpath("./a//text()")
|
|
109
|
+
body = "".join(str(x) for x in bodyxpath) if bodyxpath and isinstance(bodyxpath, list) else ""
|
|
110
|
+
result = {
|
|
111
|
+
"title": self._normalize(title),
|
|
112
|
+
"href": self._normalize_url(href),
|
|
113
|
+
"body": self._normalize(body),
|
|
114
|
+
}
|
|
115
|
+
page_results.append(result)
|
|
116
|
+
return page_results
|
|
117
|
+
|
|
118
|
+
slist = [0]
|
|
119
|
+
if max_results:
|
|
120
|
+
max_results = min(max_results, 2023)
|
|
121
|
+
slist.extend(range(23, max_results, 50))
|
|
122
|
+
try:
|
|
123
|
+
for r in self._executor.map(_text_html_page, slist):
|
|
124
|
+
results.extend(r)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
raise e
|
|
127
|
+
|
|
128
|
+
return list(self.islice(results, max_results))
|
|
129
|
+
|
|
130
|
+
def _text_lite(
|
|
131
|
+
self,
|
|
132
|
+
keywords: str,
|
|
133
|
+
region: str = "wt-wt",
|
|
134
|
+
timelimit: str | None = None,
|
|
135
|
+
max_results: int | None = None,
|
|
136
|
+
) -> list[dict[str, str]]:
|
|
137
|
+
"""Text search using lite backend."""
|
|
138
|
+
assert keywords, "keywords is mandatory"
|
|
139
|
+
|
|
140
|
+
payload = {
|
|
141
|
+
"q": keywords,
|
|
142
|
+
"s": "0",
|
|
143
|
+
"o": "json",
|
|
144
|
+
"api": "d.js",
|
|
145
|
+
"vqd": "",
|
|
146
|
+
"kl": region,
|
|
147
|
+
"bing_market": region,
|
|
148
|
+
}
|
|
149
|
+
if timelimit:
|
|
150
|
+
payload["df"] = timelimit
|
|
151
|
+
|
|
152
|
+
cache = set()
|
|
153
|
+
results: list[dict[str, str]] = []
|
|
154
|
+
|
|
155
|
+
def _text_lite_page(s: int) -> list[dict[str, str]]:
|
|
156
|
+
payload["s"] = f"{s}"
|
|
157
|
+
resp_content = self._get_url("POST", "https://lite.duckduckgo.com/lite/", data=payload).content
|
|
158
|
+
if b"No more results." in resp_content:
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
page_results = []
|
|
162
|
+
tree = self.parser.fromstring(resp_content)
|
|
163
|
+
elements = tree.xpath("//table[last()]//tr")
|
|
164
|
+
if not isinstance(elements, list):
|
|
165
|
+
return []
|
|
166
|
+
|
|
167
|
+
data = zip(self.cycle(range(1, 5)), elements)
|
|
168
|
+
for i, e in data:
|
|
169
|
+
if isinstance(e, self.parser.etree.Element):
|
|
170
|
+
if i == 1:
|
|
171
|
+
hrefxpath = e.xpath(".//a//@href")
|
|
172
|
+
href = str(hrefxpath[0]) if hrefxpath and isinstance(hrefxpath, list) else None
|
|
173
|
+
if (
|
|
174
|
+
href is None
|
|
175
|
+
or href in cache
|
|
176
|
+
or href.startswith(
|
|
177
|
+
("http://www.google.com/search?q=", "https://duckduckgo.com/y.js?ad_domain")
|
|
178
|
+
)
|
|
179
|
+
):
|
|
180
|
+
[next(data, None) for _ in range(3)] # skip block(i=1,2,3,4)
|
|
181
|
+
else:
|
|
182
|
+
cache.add(href)
|
|
183
|
+
titlexpath = e.xpath(".//a//text()")
|
|
184
|
+
title = str(titlexpath[0]) if titlexpath and isinstance(titlexpath, list) else ""
|
|
185
|
+
elif i == 2:
|
|
186
|
+
bodyxpath = e.xpath(".//td[@class='result-snippet']//text()")
|
|
187
|
+
body = (
|
|
188
|
+
"".join(str(x) for x in bodyxpath).strip()
|
|
189
|
+
if bodyxpath and isinstance(bodyxpath, list)
|
|
190
|
+
else ""
|
|
191
|
+
)
|
|
192
|
+
if href:
|
|
193
|
+
result = {
|
|
194
|
+
"title": self._normalize(title),
|
|
195
|
+
"href": self._normalize_url(href),
|
|
196
|
+
"body": self._normalize(body),
|
|
197
|
+
}
|
|
198
|
+
page_results.append(result)
|
|
199
|
+
return page_results
|
|
200
|
+
|
|
201
|
+
slist = [0]
|
|
202
|
+
if max_results:
|
|
203
|
+
max_results = min(max_results, 2023)
|
|
204
|
+
slist.extend(range(23, max_results, 50))
|
|
205
|
+
try:
|
|
206
|
+
for r in self._executor.map(_text_lite_page, slist):
|
|
207
|
+
results.extend(r)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
raise e
|
|
210
|
+
|
|
211
|
+
return list(self.islice(results, max_results))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ....exceptions import WebscoutE
|
|
4
|
+
from .base import DuckDuckGoBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DuckDuckGoTranslate(DuckDuckGoBase):
|
|
8
|
+
def run(self, *args, **kwargs) -> list[dict[str, str]]:
|
|
9
|
+
keywords = args[0] if args else kwargs.get("keywords")
|
|
10
|
+
from_ = args[1] if len(args) > 1 else kwargs.get("from_")
|
|
11
|
+
to = args[2] if len(args) > 2 else kwargs.get("to", "en")
|
|
12
|
+
|
|
13
|
+
assert keywords, "keywords is mandatory"
|
|
14
|
+
|
|
15
|
+
vqd = self._get_vqd("translate")
|
|
16
|
+
|
|
17
|
+
payload = {
|
|
18
|
+
"vqd": vqd,
|
|
19
|
+
"query": "translate",
|
|
20
|
+
"to": to,
|
|
21
|
+
}
|
|
22
|
+
if from_:
|
|
23
|
+
payload["from"] = from_
|
|
24
|
+
|
|
25
|
+
def _translate_keyword(keyword: str) -> dict[str, str]:
|
|
26
|
+
resp_content = self._get_url(
|
|
27
|
+
"POST",
|
|
28
|
+
"https://duckduckgo.com/translation.js",
|
|
29
|
+
params=payload,
|
|
30
|
+
content=keyword.encode(),
|
|
31
|
+
).content
|
|
32
|
+
page_data: dict[str, str] = self.json_loads(resp_content)
|
|
33
|
+
page_data["original"] = keyword
|
|
34
|
+
return page_data
|
|
35
|
+
|
|
36
|
+
if isinstance(keywords, str):
|
|
37
|
+
keywords = [keywords]
|
|
38
|
+
|
|
39
|
+
results = []
|
|
40
|
+
try:
|
|
41
|
+
for r in self._executor.map(_translate_keyword, keywords):
|
|
42
|
+
results.append(r)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
raise e
|
|
45
|
+
|
|
46
|
+
return results
|
|
47
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ....exceptions import WebscoutE
|
|
4
|
+
from .base import DuckDuckGoBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DuckDuckGoVideos(DuckDuckGoBase):
|
|
8
|
+
def run(self, *args, **kwargs) -> list[dict[str, str]]:
|
|
9
|
+
keywords = args[0] if args else kwargs.get("keywords")
|
|
10
|
+
region = args[1] if len(args) > 1 else kwargs.get("region", "wt-wt")
|
|
11
|
+
safesearch = args[2] if len(args) > 2 else kwargs.get("safesearch", "moderate")
|
|
12
|
+
timelimit = args[3] if len(args) > 3 else kwargs.get("timelimit")
|
|
13
|
+
resolution = args[4] if len(args) > 4 else kwargs.get("resolution")
|
|
14
|
+
duration = args[5] if len(args) > 5 else kwargs.get("duration")
|
|
15
|
+
license_videos = args[6] if len(args) > 6 else kwargs.get("license_videos")
|
|
16
|
+
max_results = args[7] if len(args) > 7 else kwargs.get("max_results")
|
|
17
|
+
|
|
18
|
+
assert keywords, "keywords is mandatory"
|
|
19
|
+
|
|
20
|
+
vqd = self._get_vqd(keywords)
|
|
21
|
+
|
|
22
|
+
safesearch_base = {"on": "1", "moderate": "-1", "off": "-2"}
|
|
23
|
+
timelimit = f"publishedAfter:{timelimit}" if timelimit else ""
|
|
24
|
+
resolution = f"videoDefinition:{resolution}" if resolution else ""
|
|
25
|
+
duration = f"videoDuration:{duration}" if duration else ""
|
|
26
|
+
license_videos = f"videoLicense:{license_videos}" if license_videos else ""
|
|
27
|
+
payload = {
|
|
28
|
+
"l": region,
|
|
29
|
+
"o": "json",
|
|
30
|
+
"q": keywords,
|
|
31
|
+
"vqd": vqd,
|
|
32
|
+
"f": f"{timelimit},{resolution},{duration},{license_videos}",
|
|
33
|
+
"p": safesearch_base[safesearch.lower()],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
cache = set()
|
|
37
|
+
results: list[dict[str, str]] = []
|
|
38
|
+
|
|
39
|
+
def _videos_page(s: int) -> list[dict[str, str]]:
|
|
40
|
+
payload["s"] = f"{s}"
|
|
41
|
+
resp_content = self._get_url("GET", "https://duckduckgo.com/v.js", params=payload).content
|
|
42
|
+
resp_json = self.json_loads(resp_content)
|
|
43
|
+
|
|
44
|
+
page_data = resp_json.get("results", [])
|
|
45
|
+
page_results = []
|
|
46
|
+
for row in page_data:
|
|
47
|
+
if row["content"] not in cache:
|
|
48
|
+
cache.add(row["content"])
|
|
49
|
+
page_results.append(row)
|
|
50
|
+
return page_results
|
|
51
|
+
|
|
52
|
+
slist = [0]
|
|
53
|
+
if max_results:
|
|
54
|
+
max_results = min(max_results, 400)
|
|
55
|
+
slist.extend(range(60, max_results, 60))
|
|
56
|
+
try:
|
|
57
|
+
for r in self._executor.map(_videos_page, slist):
|
|
58
|
+
results.extend(r)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise e
|
|
61
|
+
|
|
62
|
+
return list(self.islice(results, max_results))
|
|
63
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
from ....exceptions import WebscoutE
|
|
8
|
+
from .base import DuckDuckGoBase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DuckDuckGoWeather(DuckDuckGoBase):
|
|
12
|
+
def run(self, *args, **kwargs) -> dict[str, any]:
|
|
13
|
+
location = args[0] if args else kwargs.get("location")
|
|
14
|
+
language = args[1] if len(args) > 1 else kwargs.get("language", "en")
|
|
15
|
+
|
|
16
|
+
assert location, "location is mandatory"
|
|
17
|
+
lang = language.split('-')[0]
|
|
18
|
+
url = f"https://duckduckgo.com/js/spice/forecast/{quote(location)}/{lang}"
|
|
19
|
+
|
|
20
|
+
resp = self._get_url("GET", url).content
|
|
21
|
+
resp_text = resp.decode('utf-8')
|
|
22
|
+
|
|
23
|
+
if "ddg_spice_forecast(" not in resp_text:
|
|
24
|
+
raise WebscoutE(f"No weather data found for {location}")
|
|
25
|
+
|
|
26
|
+
json_text = resp_text[resp_text.find('(') + 1:resp_text.rfind(')')]
|
|
27
|
+
try:
|
|
28
|
+
result = json.loads(json_text)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
raise WebscoutE(f"Error parsing weather JSON: {e}")
|
|
31
|
+
|
|
32
|
+
if not result or 'currentWeather' not in result or 'forecastDaily' not in result:
|
|
33
|
+
raise WebscoutE(f"Invalid weather data format for {location}")
|
|
34
|
+
|
|
35
|
+
formatted_data = {
|
|
36
|
+
"location": result["currentWeather"]["metadata"].get("ddg-location", "Unknown"),
|
|
37
|
+
"current": {
|
|
38
|
+
"condition": result["currentWeather"].get("conditionCode"),
|
|
39
|
+
"temperature_c": result["currentWeather"].get("temperature"),
|
|
40
|
+
"feels_like_c": result["currentWeather"].get("temperatureApparent"),
|
|
41
|
+
"humidity": result["currentWeather"].get("humidity"),
|
|
42
|
+
"wind_speed_ms": result["currentWeather"].get("windSpeed"),
|
|
43
|
+
"wind_direction": result["currentWeather"].get("windDirection"),
|
|
44
|
+
"visibility_m": result["currentWeather"].get("visibility"),
|
|
45
|
+
},
|
|
46
|
+
"daily_forecast": [],
|
|
47
|
+
"hourly_forecast": []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for day in result["forecastDaily"]["days"]:
|
|
51
|
+
formatted_data["daily_forecast"].append({
|
|
52
|
+
"date": datetime.fromisoformat(day["forecastStart"].replace("Z", "+00:00")).strftime("%Y-%m-%d"),
|
|
53
|
+
"condition": day["daytimeForecast"].get("conditionCode"),
|
|
54
|
+
"max_temp_c": day["temperatureMax"],
|
|
55
|
+
"min_temp_c": day["temperatureMin"],
|
|
56
|
+
"sunrise": datetime.fromisoformat(day["sunrise"].replace("Z", "+00:00")).strftime("%H:%M"),
|
|
57
|
+
"sunset": datetime.fromisoformat(day["sunset"].replace("Z", "+00:00")).strftime("%H:%M"),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if 'forecastHourly' in result and 'hours' in result['forecastHourly']:
|
|
61
|
+
for hour in result['forecastHourly']['hours']:
|
|
62
|
+
formatted_data["hourly_forecast"].append({
|
|
63
|
+
"time": datetime.fromisoformat(hour["forecastStart"].replace("Z", "+00:00")).strftime("%H:%M"),
|
|
64
|
+
"condition": hour.get("conditionCode"),
|
|
65
|
+
"temperature_c": hour.get("temperature"),
|
|
66
|
+
"feels_like_c": hour.get("temperatureApparent"),
|
|
67
|
+
"humidity": hour.get("humidity"),
|
|
68
|
+
"wind_speed_ms": hour.get("windSpeed"),
|
|
69
|
+
"wind_direction": hour.get("windDirection"),
|
|
70
|
+
"visibility_m": hour.get("visibility"),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return formatted_data
|
|
74
|
+
|