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.
Files changed (45) hide show
  1. {scrapemm-0.5.2/scrapeMM.egg-info → scrapemm-0.5.4}/PKG-INFO +1 -1
  2. {scrapemm-0.5.2 → scrapemm-0.5.4}/pyproject.toml +1 -1
  3. {scrapemm-0.5.2 → scrapemm-0.5.4/scrapeMM.egg-info}/PKG-INFO +1 -1
  4. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapeMM.egg-info/SOURCES.txt +1 -0
  5. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/requests.py +2 -0
  6. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/__init__.py +2 -1
  7. scrapemm-0.5.4/scrapemm/integrations/archive_org.py +26 -0
  8. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/decodo.py +3 -3
  9. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/firecrawl/firecrawl.py +43 -24
  10. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/tiktok.py +1 -1
  11. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/retrieval.py +6 -0
  12. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/util.py +2 -2
  13. {scrapemm-0.5.2 → scrapemm-0.5.4}/scripts/example.py +1 -1
  14. {scrapemm-0.5.2 → scrapemm-0.5.4}/.gitignore +0 -0
  15. {scrapemm-0.5.2 → scrapemm-0.5.4}/LICENSE +0 -0
  16. {scrapemm-0.5.2 → scrapemm-0.5.4}/README.md +0 -0
  17. {scrapemm-0.5.2 → scrapemm-0.5.4}/requirements.txt +0 -0
  18. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapeMM.egg-info/dependency_links.txt +0 -0
  19. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapeMM.egg-info/requires.txt +0 -0
  20. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapeMM.egg-info/top_level.txt +0 -0
  21. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/__init__.py +0 -0
  22. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/common/__init__.py +0 -0
  23. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/common/exceptions.py +0 -0
  24. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/common/scraping_response.py +0 -0
  25. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/__init__.py +0 -0
  26. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/common.py +0 -0
  27. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/images.py +0 -0
  28. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/media.py +0 -0
  29. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/util.py +0 -0
  30. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/download/videos.py +0 -0
  31. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/base.py +0 -0
  32. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/bluesky.py +0 -0
  33. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/fb.py +0 -0
  34. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/firecrawl/__init__.py +0 -0
  35. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/firecrawl/no_bot_domains.txt +0 -0
  36. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/instagram.py +0 -0
  37. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/perma_cc.py +0 -0
  38. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/telegram.py +0 -0
  39. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/x.py +0 -0
  40. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/youtube.py +0 -0
  41. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/integrations/ytdlp.py +0 -0
  42. {scrapemm-0.5.2 → scrapemm-0.5.4}/scrapemm/secrets.py +0 -0
  43. {scrapemm-0.5.2 → scrapemm-0.5.4}/setup.cfg +0 -0
  44. {scrapemm-0.5.2 → scrapemm-0.5.4}/testing/test_retrieval.py +0 -0
  45. {scrapemm-0.5.2 → scrapemm-0.5.4}/testing/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Flexible multimodal scraper for social media and the open web.
5
5
  Author-email: Mark Rothermel <mark.rothermel@tu-darmstadt.de>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrapeMM"
3
- version = "0.5.2"
3
+ version = "0.5.4"
4
4
  description = "Flexible multimodal scraper for social media and the open web."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Flexible multimodal scraper for social media and the open web.
5
5
  Author-email: Mark Rothermel <mark.rothermel@tu-darmstadt.de>
6
6
  License-Expression: Apache-2.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 reached.")
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
- try:
82
- document = await self._firecrawl.scrape(
83
- url,
84
- formats=["html"],
85
- only_main_content=False,
86
- remove_base64_images=False,
87
- exclude_tags=["script", "style", "noscript", "footer", "aside"],
88
- timeout=30_000,
89
- wait_for=1_000,
90
- store_in_cache=False,
91
- **kwargs
92
- )
93
- except Exception as e:
94
- # Ensure firecrawl is still running
95
- if not await firecrawl_is_running(self.firecrawl_url):
96
- logger.error(f"❌ Firecrawl stopped running at {self.firecrawl_url}.")
97
- raise RuntimeError("Firecrawl stopped running.")
98
- else:
99
- raise e
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 False
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=1) as response:
140
- return 200 <= response.status < 400
141
- except (aiohttp.ClientError, ConnectionError, ReadTimeout, RetryError, asyncio.TimeoutError):
142
- return False
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"
@@ -28,7 +28,7 @@ class TikTok(RetrievalIntegration):
28
28
  """
29
29
 
30
30
  name = "TikTok"
31
- domains = ["tiktok.com", "vm.tiktok.com"]
31
+ domains = ["tiktok.com"]
32
32
 
33
33
  async def _connect(self):
34
34
  # Try to initialize TikTok Research API
@@ -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://factuel.afp.com/doc.afp.com.43ZN7NP"
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