scrapeMM 0.2.2__tar.gz → 0.3.1__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 (39) hide show
  1. {scrapemm-0.2.2 → scrapemm-0.3.1}/.gitignore +2 -1
  2. {scrapemm-0.2.2/scrapeMM.egg-info → scrapemm-0.3.1}/PKG-INFO +6 -4
  3. {scrapemm-0.2.2 → scrapemm-0.3.1}/README.md +4 -3
  4. {scrapemm-0.2.2 → scrapemm-0.3.1}/pyproject.toml +2 -1
  5. {scrapemm-0.2.2 → scrapemm-0.3.1}/requirements.txt +3 -2
  6. {scrapemm-0.2.2 → scrapemm-0.3.1/scrapeMM.egg-info}/PKG-INFO +6 -4
  7. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapeMM.egg-info/SOURCES.txt +2 -0
  8. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapeMM.egg-info/requires.txt +1 -0
  9. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/integrations/__init__.py +5 -9
  10. scrapemm-0.3.1/scrapemm/integrations/base.py +39 -0
  11. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/integrations/bluesky.py +7 -11
  12. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/integrations/fb.py +3 -7
  13. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/integrations/instagram.py +4 -7
  14. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/integrations/telegram.py +13 -5
  15. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/integrations/tiktok.py +52 -55
  16. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/integrations/x.py +4 -4
  17. scrapemm-0.3.1/scrapemm/integrations/youtube.py +34 -0
  18. scrapemm-0.3.1/scrapemm/retrieval.py +128 -0
  19. scrapemm-0.3.1/scrapemm/scraping/__init__.py +2 -0
  20. scrapemm-0.3.1/scrapemm/scraping/decodo.py +195 -0
  21. scrapemm-0.3.1/scrapemm/scraping/firecrawl.py +100 -0
  22. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/scraping/util.py +15 -10
  23. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/scraping/ytdlp.py +24 -11
  24. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/secrets.py +22 -11
  25. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/util.py +1 -1
  26. {scrapemm-0.2.2 → scrapemm-0.3.1}/testing/test_retrieval.py +58 -7
  27. scrapemm-0.2.2/scrapemm/integrations/base.py +0 -16
  28. scrapemm-0.2.2/scrapemm/retrieval.py +0 -65
  29. scrapemm-0.2.2/scrapemm/scraping/__init__.py +0 -0
  30. scrapemm-0.2.2/scrapemm/scraping/firecrawl.py +0 -157
  31. {scrapemm-0.2.2 → scrapemm-0.3.1}/LICENSE +0 -0
  32. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapeMM.egg-info/dependency_links.txt +0 -0
  33. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapeMM.egg-info/top_level.txt +0 -0
  34. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/__init__.py +0 -0
  35. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/common.py +0 -0
  36. {scrapemm-0.2.2 → scrapemm-0.3.1}/scrapemm/scraping/no_bot_domains.txt +0 -0
  37. {scrapemm-0.2.2 → scrapemm-0.3.1}/scripts/example.py +0 -0
  38. {scrapemm-0.2.2 → scrapemm-0.3.1}/setup.cfg +0 -0
  39. {scrapemm-0.2.2 → scrapemm-0.3.1}/testing/test_utils.py +0 -0
@@ -3,4 +3,5 @@
3
3
  /scrapeMM.egg-info/
4
4
  /dist/
5
5
  /config.yaml
6
- __pycache__/
6
+ __pycache__/
7
+ .DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: LLM-friendly scraper for media and text from social media and the open web.
5
5
  Author-email: Mark Rothermel <mark.rothermel@tu-darmstadt.de>
6
6
  License-Expression: Apache-2.0
@@ -19,6 +19,7 @@ Requires-Dist: atproto
19
19
  Requires-Dist: TikTokResearchApi
20
20
  Requires-Dist: yt-dlp
21
21
  Requires-Dist: cryptography
22
+ Requires-Dist: firecrawl-py
22
23
  Dynamic: license-file
23
24
 
24
25
  # scrapeMM: Multimodal Web Retrieval
@@ -46,14 +47,15 @@ URL (string) --> retrieve() --> MultimodalSequence
46
47
  ```
47
48
  The `MultimodalSequence` is a sequence of Markdown-formatted text and media provided by the [ezMM](https://github.com/multimodal-ai-lab/ezmm) library.
48
49
 
49
- Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl).
50
+ Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl) and [Decodo](https://decodo.com/).
50
51
 
51
52
  ## Supported Proprietary APIs
52
53
  - ✅ X/Twitter
53
54
  - ✅ Telegram
54
55
  - ✅ Bluesky
55
56
  - ✅ TikTok
57
+ - ⚠️ Facebook (working only sometimes and only with yt-dlp and Decodo)
58
+ - ⚠️ Instagram (done for videos but not for images yet)
59
+ - ⚠️ YouTube (working sometimes)
56
60
  - ⏳ Threads
57
61
  - ⏳ Reddit
58
- - ⏳ Facebook
59
- - ⏳ Instagram
@@ -23,14 +23,15 @@ URL (string) --> retrieve() --> MultimodalSequence
23
23
  ```
24
24
  The `MultimodalSequence` is a sequence of Markdown-formatted text and media provided by the [ezMM](https://github.com/multimodal-ai-lab/ezmm) library.
25
25
 
26
- Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl).
26
+ Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl) and [Decodo](https://decodo.com/).
27
27
 
28
28
  ## Supported Proprietary APIs
29
29
  - ✅ X/Twitter
30
30
  - ✅ Telegram
31
31
  - ✅ Bluesky
32
32
  - ✅ TikTok
33
+ - ⚠️ Facebook (working only sometimes and only with yt-dlp and Decodo)
34
+ - ⚠️ Instagram (done for videos but not for images yet)
35
+ - ⚠️ YouTube (working sometimes)
33
36
  - ⏳ Threads
34
37
  - ⏳ Reddit
35
- - ⏳ Facebook
36
- - ⏳ Instagram
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrapeMM"
3
- version = "0.2.2"
3
+ version = "0.3.1"
4
4
  description = "LLM-friendly scraper for media and text from social media and the open web."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
@@ -17,6 +17,7 @@ dependencies = [
17
17
  "TikTokResearchApi",
18
18
  "yt-dlp",
19
19
  "cryptography",
20
+ "firecrawl-py"
20
21
  ]
21
22
 
22
23
  [project.urls]
@@ -2,7 +2,7 @@ pytest~=8.4.1
2
2
  aiohttp~=3.12.13
3
3
  numpy~=1.26.4
4
4
  requests~=2.32.4
5
- ezMM~=0.3.1
5
+ ezMM~=0.3.4
6
6
  Telethon~=1.40.0
7
7
  PyYAML~=6.0.1
8
8
  tweepy~=4.16.0
@@ -15,4 +15,5 @@ platformdirs~=4.3.8
15
15
  atproto
16
16
  TikTokResearchApi
17
17
  yt-dlp
18
- cryptography
18
+ cryptography
19
+ firecrawl-py~=4.6.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: LLM-friendly scraper for media and text from social media and the open web.
5
5
  Author-email: Mark Rothermel <mark.rothermel@tu-darmstadt.de>
6
6
  License-Expression: Apache-2.0
@@ -19,6 +19,7 @@ Requires-Dist: atproto
19
19
  Requires-Dist: TikTokResearchApi
20
20
  Requires-Dist: yt-dlp
21
21
  Requires-Dist: cryptography
22
+ Requires-Dist: firecrawl-py
22
23
  Dynamic: license-file
23
24
 
24
25
  # scrapeMM: Multimodal Web Retrieval
@@ -46,14 +47,15 @@ URL (string) --> retrieve() --> MultimodalSequence
46
47
  ```
47
48
  The `MultimodalSequence` is a sequence of Markdown-formatted text and media provided by the [ezMM](https://github.com/multimodal-ai-lab/ezmm) library.
48
49
 
49
- Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl).
50
+ Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl) and [Decodo](https://decodo.com/).
50
51
 
51
52
  ## Supported Proprietary APIs
52
53
  - ✅ X/Twitter
53
54
  - ✅ Telegram
54
55
  - ✅ Bluesky
55
56
  - ✅ TikTok
57
+ - ⚠️ Facebook (working only sometimes and only with yt-dlp and Decodo)
58
+ - ⚠️ Instagram (done for videos but not for images yet)
59
+ - ⚠️ YouTube (working sometimes)
56
60
  - ⏳ Threads
57
61
  - ⏳ Reddit
58
- - ⏳ Facebook
59
- - ⏳ Instagram
@@ -21,7 +21,9 @@ scrapemm/integrations/instagram.py
21
21
  scrapemm/integrations/telegram.py
22
22
  scrapemm/integrations/tiktok.py
23
23
  scrapemm/integrations/x.py
24
+ scrapemm/integrations/youtube.py
24
25
  scrapemm/scraping/__init__.py
26
+ scrapemm/scraping/decodo.py
25
27
  scrapemm/scraping/firecrawl.py
26
28
  scrapemm/scraping/no_bot_domains.txt
27
29
  scrapemm/scraping/util.py
@@ -8,3 +8,4 @@ atproto
8
8
  TikTokResearchApi
9
9
  yt-dlp
10
10
  cryptography
11
+ firecrawl-py
@@ -1,8 +1,3 @@
1
- import logging
2
-
3
- logger = logging.getLogger("scrapeMM")
4
- logger.debug("Starting integrations...")
5
-
6
1
  from typing import Optional
7
2
 
8
3
  import aiohttp
@@ -15,16 +10,17 @@ from .instagram import Instagram
15
10
  from .telegram import Telegram
16
11
  from .tiktok import TikTok
17
12
  from .x import X
13
+ from .youtube import YouTube
18
14
 
19
- RETRIEVAL_INTEGRATIONS = [X(), Telegram(), Bluesky(), TikTok(), Instagram(), Facebook()]
15
+ RETRIEVAL_INTEGRATIONS = [X(), Telegram(), Bluesky(), TikTok(), Instagram(), Facebook(), YouTube()]
20
16
  DOMAIN_TO_INTEGRATION = {domain: integration
21
17
  for integration in RETRIEVAL_INTEGRATIONS
22
- for domain in integration.domains
23
- if integration.connected}
18
+ for domain in integration.domains}
24
19
 
25
20
 
26
21
  async def retrieve_via_integration(url: str, session: aiohttp.ClientSession) -> Optional[MultimodalSequence]:
27
22
  domain = get_domain(url)
28
23
  if domain in DOMAIN_TO_INTEGRATION:
29
24
  integration = DOMAIN_TO_INTEGRATION[domain]
30
- return await integration.get(url, session)
25
+ if integration.connected or integration.connected is None:
26
+ return await integration.get(url, session)
@@ -0,0 +1,39 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
3
+
4
+ import aiohttp
5
+ from ezmm import MultimodalSequence
6
+
7
+ from scrapemm.common import logger
8
+ from scrapemm.util import get_domain
9
+
10
+
11
+ class RetrievalIntegration(ABC):
12
+ """Any integration used to retrieve information via an external API. Typically
13
+ used when direct URL scraping is not possible."""
14
+
15
+ name: str
16
+ domains: list[str] # The domains supported by this integration
17
+ connected: bool = None
18
+
19
+ @abstractmethod
20
+ async def _connect(self):
21
+ """Establish a connection to the external service. Invoked upon the first get() call.
22
+ Must set self.connet = True if connection was successful, else False."""
23
+ raise NotImplementedError
24
+
25
+ async def get(self, url: str, session: aiohttp.ClientSession) -> Optional[MultimodalSequence]:
26
+ """Ensures connectivity before invoking the service for retrieval."""
27
+ assert get_domain(url) in self.domains
28
+ if self.connected is None:
29
+ await self._connect()
30
+ if not self.connected:
31
+ logger.warning(f"❌ Connection to {self.name} service could not be established.")
32
+ if self.connected:
33
+ logger.debug(f"Calling {self.name} service for {url}")
34
+ return await self._get(url, session)
35
+
36
+ @abstractmethod
37
+ async def _get(self, url: str, session: aiohttp.ClientSession) -> Optional[MultimodalSequence]:
38
+ """Retrieves the contents present at the given URL."""
39
+ raise NotImplementedError
@@ -2,8 +2,6 @@ import logging
2
2
  from typing import Optional
3
3
 
4
4
  import aiohttp
5
- from atproto_client.exceptions import RequestErrorBase
6
- from atproto_client.models.common import XrpcError
7
5
  from ezmm import MultimodalSequence, download_image, download_video
8
6
 
9
7
  from scrapemm.secrets import get_secret
@@ -14,9 +12,10 @@ logger = logging.getLogger("scrapeMM")
14
12
 
15
13
 
16
14
  class Bluesky(RetrievalIntegration):
15
+ name = "Bluesky"
17
16
  domains = ["bsky.app"]
18
17
 
19
- def __init__(self):
18
+ async def _connect(self):
20
19
  self.username = get_secret("bluesky_username")
21
20
  self.password = get_secret("bluesky_password")
22
21
 
@@ -27,14 +26,9 @@ class Bluesky(RetrievalIntegration):
27
26
 
28
27
  from atproto import Client
29
28
  self.client = Client()
30
- self.authenticated = False
31
29
  self._authenticate()
32
30
 
33
- async def get(self, url: str, session: aiohttp.ClientSession) -> Optional[MultimodalSequence]:
34
- if not self.authenticated:
35
- logger.error("❌ Not authenticated with Bluesky. Cannot retrieve content.")
36
- return None
37
-
31
+ async def _get(self, url: str, session: aiohttp.ClientSession) -> Optional[MultimodalSequence]:
38
32
  if get_domain(url) not in self.domains:
39
33
  logger.error(f"❌ Invalid domain for Bluesky: {get_domain(url)}")
40
34
  return None
@@ -169,7 +163,7 @@ Metrics:
169
163
  """Authenticate with Bluesky using provided credentials."""
170
164
  try:
171
165
  self.client.login(self.username, self.password)
172
- self.authenticated = True
166
+ self.connected = True
173
167
  logger.info(f"✅ Successfully authenticated with Bluesky as {self.username}")
174
168
  return True
175
169
  except Exception as e:
@@ -221,12 +215,14 @@ Metrics:
221
215
  return handle # Return the handle itself as fallback
222
216
 
223
217
 
224
- def error_to_string(error: RequestErrorBase | Exception) -> str:
218
+ def error_to_string(error: Exception) -> str:
225
219
  """Takes an Error object containing a response and prints the contents."""
220
+ from atproto_client.exceptions import RequestErrorBase
226
221
  if isinstance(error, RequestErrorBase):
227
222
  response = error.response
228
223
  code = response.status_code
229
224
  content = response.content
225
+ from atproto_client.models.common import XrpcError
230
226
  if isinstance(content, XrpcError):
231
227
  error_type = content.error
232
228
  msg = content.message
@@ -11,10 +11,10 @@ logger = logging.getLogger("scrapeMM")
11
11
 
12
12
 
13
13
  class Facebook(RetrievalIntegration):
14
-
14
+ name = "Facebook"
15
15
  domains = ["facebook.com", "www.facebook.com"]
16
16
 
17
- def __init__(self):
17
+ async def _connect(self):
18
18
  # Check if yt-dlp is available
19
19
  self.ytdlp_available = check_ytdlp_available()
20
20
 
@@ -26,12 +26,8 @@ class Facebook(RetrievalIntegration):
26
26
  self.connected = False
27
27
  logger.warning("❌ Facebook integration not available: Neither API credentials nor yt-dlp found.")
28
28
 
29
- async def get(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
29
+ async def _get(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
30
30
  """Retrieves content from a Facebook post URL."""
31
- if not self.connected:
32
- logger.error("❌ Facebook integration not connected. Cannot retrieve content.")
33
- return None
34
-
35
31
  if get_domain(url) not in self.domains:
36
32
  logger.error(f"❌ Invalid domain for Facebook: {get_domain(url)}")
37
33
  return None
@@ -11,9 +11,10 @@ logger = logging.getLogger("scrapeMM")
11
11
 
12
12
 
13
13
  class Instagram(RetrievalIntegration):
14
+ name = "Instagram"
14
15
  domains = ["instagram.com", "www.instagram.com"]
15
16
 
16
- def __init__(self):
17
+ async def _connect(self):
17
18
  # Check if yt-dlp is available
18
19
  self.ytdlp_available = check_ytdlp_available()
19
20
 
@@ -25,12 +26,8 @@ class Instagram(RetrievalIntegration):
25
26
  self.connected = False
26
27
  logger.warning("❌ Instagram integration not available: Neither API credentials nor yt-dlp found.")
27
28
 
28
- async def get(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
29
+ async def _get(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
29
30
  """Retrieves content from an Instagram post URL."""
30
- if not self.connected:
31
- logger.error("❌ Instagram integration not connected. Cannot retrieve content.")
32
- return None
33
-
34
31
  if get_domain(url) not in self.domains:
35
32
  logger.error(f"❌ Invalid domain for Instagram: {get_domain(url)}")
36
33
  return None
@@ -72,7 +69,7 @@ Configure API credentials for full profile information."""
72
69
 
73
70
  def _is_video_url(self, url: str) -> bool:
74
71
  """Checks if the URL is an Instagram video URL."""
75
- return "instagram.com/reels" in url
72
+ return "instagram.com/reels" in url or "instagram.com/reel/" in url
76
73
 
77
74
  def _is_photo_url(self, url: str) -> bool:
78
75
  """Checks if the URL is an Instagram photo URL."""
@@ -1,6 +1,8 @@
1
1
  import logging
2
+ from pathlib import Path
2
3
  from typing import Optional
3
4
  from urllib.parse import urlparse
5
+ import sqlite3
4
6
 
5
7
  import aiohttp
6
8
  from ezmm import MultimodalSequence, Image, Item, Video
@@ -17,23 +19,30 @@ logger = logging.getLogger("scrapeMM")
17
19
  class Telegram(RetrievalIntegration):
18
20
  """The Telegram integration for retrieving post contents from Telegram channels and groups."""
19
21
 
22
+ name = "Telegram"
20
23
  domains = ["t.me", "telegram.me"]
21
24
  session_path = "temp/telegram"
22
25
 
23
- def __init__(self):
26
+ async def _connect(self):
24
27
  api_id = int(get_secret("telegram_api_id")) if get_secret("telegram_api_id") else None
25
28
  api_hash = get_secret("telegram_api_hash")
26
29
  bot_token = get_secret("telegram_bot_token")
27
30
 
28
31
  if api_id and api_hash and bot_token:
29
32
  self.client = TelegramClient(self.session_path, api_id, api_hash)
30
- self.client.start(bot_token=bot_token)
33
+ try:
34
+ self.client.start(bot_token=bot_token)
35
+ except sqlite3.OperationalError: # Database is locked from an interrupted previous session
36
+ # Remove the database file and try again
37
+ journal_path = Path(self.session_path + ".session-journal")
38
+ journal_path.unlink(missing_ok=True)
31
39
  self.connected = True
32
40
  logger.info("✅ Successfully connected to Telegram.")
33
41
  else:
42
+ self.connected = False
34
43
  logger.warning("❌ Telegram integration not configured: Missing API keys.")
35
44
 
36
- async def get(self, url: str, session: aiohttp.ClientSession) -> Optional[MultimodalSequence]:
45
+ async def _get(self, url: str, session: aiohttp.ClientSession) -> Optional[MultimodalSequence]:
37
46
  """Retrieves content from a Telegram post URL."""
38
47
  assert get_domain(url) in self.domains
39
48
 
@@ -92,8 +101,7 @@ Forwards: {message.forwards}{reactions_text}
92
101
  return MultimodalSequence(text)
93
102
 
94
103
  except Exception as e:
95
- print(f"Error retrieving Telegram content: {e}")
96
- # raise
104
+ logger.warning(f"Error retrieving Telegram content: {e}")
97
105
  return None
98
106
 
99
107
  async def _get_media_from_message(self, chat, original_post, max_amp=10) -> list[Item]: