scrapeMM 0.5.2__tar.gz → 0.5.4__tar.gz
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.
- {scrapemm-0.5.2/scrapeMM.egg-info → scrapemm-0.5.4}/PKG-INFO +1 -1
- {scrapemm-0.5.2 → scrapemm-0.5.4}/pyproject.toml +1 -1
- {scrapemm-0.5.2 → scrapemm-0.5.4/scrapeMM.egg-info}/PKG-INFO +1 -1
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapeMM.egg-info/SOURCES.txt +1 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/requests.py +2 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/__init__.py +2 -1
- scrapemm-0.5.4/scrapemm/integrations/archive_org.py +26 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/decodo.py +3 -3
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/firecrawl/firecrawl.py +43 -24
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/tiktok.py +1 -1
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/retrieval.py +6 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/util.py +2 -2
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scripts/example.py +1 -1
- {scrapemm-0.5.2 → scrapemm-0.5.4}/.gitignore +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/LICENSE +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/README.md +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/requirements.txt +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapeMM.egg-info/dependency_links.txt +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapeMM.egg-info/requires.txt +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapeMM.egg-info/top_level.txt +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/__init__.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/common/__init__.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/common/exceptions.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/common/scraping_response.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/__init__.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/common.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/images.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/media.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/util.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/videos.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/base.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/bluesky.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/fb.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/firecrawl/__init__.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/firecrawl/no_bot_domains.txt +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/instagram.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/perma_cc.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/telegram.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/x.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/youtube.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/ytdlp.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/secrets.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/setup.cfg +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/testing/test_retrieval.py +0 -0
- {scrapemm-0.5.2 → scrapemm-0.5.4}/testing/test_utils.py +0 -0
|
@@ -23,6 +23,7 @@ scrapemm/download/requests.py
|
|
|
23
23
|
scrapemm/download/util.py
|
|
24
24
|
scrapemm/download/videos.py
|
|
25
25
|
scrapemm/integrations/__init__.py
|
|
26
|
+
scrapemm/integrations/archive_org.py
|
|
26
27
|
scrapemm/integrations/base.py
|
|
27
28
|
scrapemm/integrations/bluesky.py
|
|
28
29
|
scrapemm/integrations/decodo.py
|
|
@@ -55,6 +55,8 @@ async def request_static(url: str,
|
|
|
55
55
|
pass # Page not readable
|
|
56
56
|
except (aiohttp.ClientOSError, aiohttp.ClientConnectorError):
|
|
57
57
|
pass # Page not available anymore
|
|
58
|
+
except aiohttp.ServerDisconnectedError:
|
|
59
|
+
pass # Server aborted the connection
|
|
58
60
|
except aiohttp.ClientResponseError as e:
|
|
59
61
|
if e.status in [403, 404, 429, 500, 502, 503]:
|
|
60
62
|
# 403: Forbidden access
|
|
@@ -3,6 +3,7 @@ from typing import Optional
|
|
|
3
3
|
from ezmm import MultimodalSequence
|
|
4
4
|
|
|
5
5
|
from scrapemm.util import get_domain
|
|
6
|
+
from .archive_org import ArchiveOrg
|
|
6
7
|
from .bluesky import Bluesky
|
|
7
8
|
from .decodo import Decodo, decodo
|
|
8
9
|
from .fb import Facebook
|
|
@@ -14,7 +15,7 @@ from .x import X
|
|
|
14
15
|
from .youtube import YouTube
|
|
15
16
|
from .perma_cc import PermaCC
|
|
16
17
|
|
|
17
|
-
RETRIEVAL_INTEGRATIONS = [X(), Telegram(), Bluesky(), TikTok(), Instagram(), Facebook(), YouTube(), PermaCC()]
|
|
18
|
+
RETRIEVAL_INTEGRATIONS = [X(), Telegram(), Bluesky(), TikTok(), Instagram(), Facebook(), YouTube(), PermaCC(), ArchiveOrg()]
|
|
18
19
|
DOMAIN_TO_INTEGRATION = {domain: integration
|
|
19
20
|
for integration in RETRIEVAL_INTEGRATIONS
|
|
20
21
|
for domain in integration.domains}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from ezmm import MultimodalSequence
|
|
4
|
+
|
|
5
|
+
from scrapemm.integrations.base import RetrievalIntegration
|
|
6
|
+
from scrapemm.integrations.ytdlp import get_content_with_ytdlp
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("scrapeMM")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ArchiveOrg(RetrievalIntegration):
|
|
12
|
+
"""Integration for retrieving content from archive.org.
|
|
13
|
+
TODO: Not working yet, but yt-dlp might support this soon:
|
|
14
|
+
https://github.com/yt-dlp/yt-dlp-master-builds/releases/tag/2025.12.12.222600"""
|
|
15
|
+
name = "Internet Archive"
|
|
16
|
+
domains = ["archive.org"]
|
|
17
|
+
|
|
18
|
+
async def _connect(self):
|
|
19
|
+
self.connected = True
|
|
20
|
+
|
|
21
|
+
async def _get(self, url: str, **kwargs) -> MultimodalSequence | None:
|
|
22
|
+
return await get_content_with_ytdlp(
|
|
23
|
+
url,
|
|
24
|
+
platform="Internet Archive",
|
|
25
|
+
**kwargs
|
|
26
|
+
)
|
|
@@ -137,14 +137,14 @@ class Decodo:
|
|
|
137
137
|
if response.status == 429: # Rate limit
|
|
138
138
|
if attempt >= max_retries:
|
|
139
139
|
logger.warning(f"Error 429: Rate limit hit and maximum retries reached.")
|
|
140
|
-
raise RateLimitError(f"Decodo rate limit
|
|
140
|
+
raise RateLimitError(f"Decodo rate limit hit (despite {max_retries} retries).")
|
|
141
141
|
elif response.status == 613:
|
|
142
142
|
if attempt >= max_retries:
|
|
143
|
-
raise RuntimeError("Decodo API error 613.")
|
|
143
|
+
raise RuntimeError(f"Decodo API error 613 (despite {max_retries} retries).")
|
|
144
144
|
elif response.status == 502: # Bad gateway
|
|
145
145
|
if attempt >= max_retries:
|
|
146
146
|
logger.warning(f"Error 502: Bad gateway and maximum retries reached.")
|
|
147
|
-
raise RuntimeError("Decodo API error 502: Bad gateway.")
|
|
147
|
+
raise RuntimeError(f"Decodo API error 502: Bad gateway (despite {max_retries} retries).")
|
|
148
148
|
|
|
149
149
|
else: # Other errors that don't go away on retry
|
|
150
150
|
match response.status:
|
|
@@ -71,6 +71,7 @@ class Firecrawl:
|
|
|
71
71
|
remove_urls: bool,
|
|
72
72
|
session: aiohttp.ClientSession,
|
|
73
73
|
format: str,
|
|
74
|
+
max_attempts: int = 3,
|
|
74
75
|
**kwargs) -> Optional[MultimodalSequence | str]:
|
|
75
76
|
if is_no_bot_site(url):
|
|
76
77
|
raise ValueError(f"Firecrawl cannot scrape sites from {get_domain(url)}")
|
|
@@ -78,25 +79,35 @@ class Firecrawl:
|
|
|
78
79
|
if not self._firecrawl:
|
|
79
80
|
await self.connect()
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
document = None
|
|
83
|
+
for attempt in range(max_attempts):
|
|
84
|
+
try:
|
|
85
|
+
document = await self._firecrawl.scrape(
|
|
86
|
+
url,
|
|
87
|
+
formats=["html"],
|
|
88
|
+
only_main_content=False,
|
|
89
|
+
remove_base64_images=False,
|
|
90
|
+
exclude_tags=["script", "style", "noscript", "footer", "aside"],
|
|
91
|
+
timeout=30_000,
|
|
92
|
+
wait_for=1_000,
|
|
93
|
+
store_in_cache=False,
|
|
94
|
+
**kwargs
|
|
95
|
+
)
|
|
96
|
+
break
|
|
97
|
+
except Exception as e:
|
|
98
|
+
# Ensure firecrawl is still running
|
|
99
|
+
state = await get_firecrawl_state(self.firecrawl_url)
|
|
100
|
+
if state == "unavailable":
|
|
101
|
+
logger.error(f"❌ Firecrawl stopped running at {self.firecrawl_url}.")
|
|
102
|
+
raise RuntimeError("Firecrawl stopped running.")
|
|
103
|
+
elif state == "busy":
|
|
104
|
+
if attempt < max_attempts - 1:
|
|
105
|
+
logger.warning(f"⚠️ Firecrawl seems busy. Retrying in 10 seconds...")
|
|
106
|
+
await asyncio.sleep(10)
|
|
107
|
+
else:
|
|
108
|
+
raise e
|
|
109
|
+
else:
|
|
110
|
+
raise e
|
|
100
111
|
|
|
101
112
|
self.n_scrapes += 1
|
|
102
113
|
html = document.html
|
|
@@ -128,15 +139,23 @@ async def find_firecrawl(urls):
|
|
|
128
139
|
|
|
129
140
|
async def firecrawl_is_running(url: str) -> bool:
|
|
130
141
|
"""Returns True iff Firecrawl can be successfully pinged at the specified URL."""
|
|
142
|
+
return await get_firecrawl_state(url) == "running"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def get_firecrawl_state(url: str) -> str | None:
|
|
146
|
+
"""Returns the state of Firecrawl at the specified URL."""
|
|
131
147
|
if not url:
|
|
132
|
-
return
|
|
148
|
+
return None
|
|
133
149
|
if not url.startswith("http"):
|
|
134
150
|
url = "https://" + url
|
|
135
151
|
|
|
136
152
|
async with aiohttp.ClientSession(headers=HEADERS) as session:
|
|
137
153
|
# Retrieve the head of the homepage
|
|
138
154
|
try:
|
|
139
|
-
async with session.head(url, timeout=
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
async with session.head(url, timeout=2) as response:
|
|
156
|
+
if 200 <= response.status < 400:
|
|
157
|
+
return "running"
|
|
158
|
+
except (ReadTimeout, asyncio.TimeoutError):
|
|
159
|
+
return "busy"
|
|
160
|
+
except (aiohttp.ClientError, ConnectionError, RetryError):
|
|
161
|
+
return "unavailable"
|
|
@@ -30,11 +30,16 @@ BEST_METHODS = {
|
|
|
30
30
|
"twitter.com": ["integrations"],
|
|
31
31
|
"t.co": ["integrations"],
|
|
32
32
|
"t.me": ["integrations"],
|
|
33
|
+
"tiktok.com": ["integrations"],
|
|
33
34
|
"telegram.me": ["integrations"],
|
|
35
|
+
"bsky.app": ["integrations"],
|
|
34
36
|
"truthsocial.com": ["firecrawl"],
|
|
35
37
|
"reddit.com": ["integrations"],
|
|
36
38
|
"awesomescreenshot.com": ["firecrawl"],
|
|
37
39
|
"snopes.com": ["decodo"],
|
|
40
|
+
"youtube.com": ["integrations"],
|
|
41
|
+
"youtu.be": ["integrations"],
|
|
42
|
+
"perma.cc": ["integrations"],
|
|
38
43
|
# Archive.today:
|
|
39
44
|
"archive.today": ["decodo"],
|
|
40
45
|
"archive.is": ["decodo"],
|
|
@@ -53,6 +58,7 @@ async def retrieve(
|
|
|
53
58
|
actions: list[dict] | None = None,
|
|
54
59
|
methods: Literal["auto"] | list[str] | list[Literal["auto"] | list[str]] | None = "auto",
|
|
55
60
|
format: str = "multimodal_sequence",
|
|
61
|
+
include_media: bool = True, # TODO
|
|
56
62
|
max_video_size: int | None = None,
|
|
57
63
|
) -> ScrapingResponse | list[ScrapingResponse]:
|
|
58
64
|
"""Main function of this repository. Downloads the contents present at the given URL(s).
|
|
@@ -240,9 +240,9 @@ def normalize_video(video: Video):
|
|
|
240
240
|
input_path = video.file_path
|
|
241
241
|
output_path = input_path.with_suffix(".normalized.mp4")
|
|
242
242
|
|
|
243
|
-
meta = probe_video(input_path)
|
|
244
|
-
|
|
245
243
|
try:
|
|
244
|
+
meta = probe_video(input_path)
|
|
245
|
+
|
|
246
246
|
# Case 1: fully browser-safe → only ensure faststart
|
|
247
247
|
if is_browser_safe(meta):
|
|
248
248
|
run_command([
|
|
@@ -2,7 +2,7 @@ from scrapemm import retrieve
|
|
|
2
2
|
import asyncio
|
|
3
3
|
|
|
4
4
|
if __name__ == "__main__":
|
|
5
|
-
url = "https://
|
|
5
|
+
url = "https://web.archive.org/web/20210604181412/https://www.tiktok.com/@realstewpeters/video/6969789589590379781?is_copy_url=1"
|
|
6
6
|
result = asyncio.run(retrieve(url))
|
|
7
7
|
if result.errors:
|
|
8
8
|
print(result.errors)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|