scrapeMM 0.2.2__tar.gz → 0.3.0__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.2.2 → scrapemm-0.3.0}/.gitignore +2 -1
- {scrapemm-0.2.2/scrapeMM.egg-info → scrapemm-0.3.0}/PKG-INFO +5 -4
- {scrapemm-0.2.2 → scrapemm-0.3.0}/README.md +4 -3
- {scrapemm-0.2.2 → scrapemm-0.3.0}/pyproject.toml +1 -1
- {scrapemm-0.2.2 → scrapemm-0.3.0}/requirements.txt +3 -2
- {scrapemm-0.2.2 → scrapemm-0.3.0/scrapeMM.egg-info}/PKG-INFO +5 -4
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapeMM.egg-info/SOURCES.txt +2 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/integrations/__init__.py +5 -9
- scrapemm-0.3.0/scrapemm/integrations/base.py +39 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/integrations/bluesky.py +7 -11
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/integrations/fb.py +3 -7
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/integrations/instagram.py +4 -7
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/integrations/telegram.py +13 -5
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/integrations/tiktok.py +52 -55
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/integrations/x.py +4 -4
- scrapemm-0.3.0/scrapemm/integrations/youtube.py +34 -0
- scrapemm-0.3.0/scrapemm/retrieval.py +118 -0
- scrapemm-0.3.0/scrapemm/scraping/__init__.py +2 -0
- scrapemm-0.3.0/scrapemm/scraping/decodo.py +191 -0
- scrapemm-0.3.0/scrapemm/scraping/firecrawl.py +96 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/scraping/util.py +15 -10
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/scraping/ytdlp.py +24 -11
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/secrets.py +22 -11
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/util.py +1 -1
- {scrapemm-0.2.2 → scrapemm-0.3.0}/testing/test_retrieval.py +38 -1
- scrapemm-0.2.2/scrapemm/integrations/base.py +0 -16
- scrapemm-0.2.2/scrapemm/retrieval.py +0 -65
- scrapemm-0.2.2/scrapemm/scraping/__init__.py +0 -0
- scrapemm-0.2.2/scrapemm/scraping/firecrawl.py +0 -157
- {scrapemm-0.2.2 → scrapemm-0.3.0}/LICENSE +0 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapeMM.egg-info/dependency_links.txt +0 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapeMM.egg-info/requires.txt +0 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapeMM.egg-info/top_level.txt +0 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/__init__.py +0 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/common.py +0 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scrapemm/scraping/no_bot_domains.txt +0 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/scripts/example.py +0 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/setup.cfg +0 -0
- {scrapemm-0.2.2 → scrapemm-0.3.0}/testing/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrapeMM
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -46,14 +46,15 @@ URL (string) --> retrieve() --> MultimodalSequence
|
|
|
46
46
|
```
|
|
47
47
|
The `MultimodalSequence` is a sequence of Markdown-formatted text and media provided by the [ezMM](https://github.com/multimodal-ai-lab/ezmm) library.
|
|
48
48
|
|
|
49
|
-
Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl).
|
|
49
|
+
Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl) and [Decodo](https://decodo.com/).
|
|
50
50
|
|
|
51
51
|
## Supported Proprietary APIs
|
|
52
52
|
- ✅ X/Twitter
|
|
53
53
|
- ✅ Telegram
|
|
54
54
|
- ✅ Bluesky
|
|
55
55
|
- ✅ TikTok
|
|
56
|
+
- ⚠️ Facebook (working only sometimes and only with yt-dlp and Decodo)
|
|
57
|
+
- ⚠️ Instagram (done for videos but not for images yet)
|
|
58
|
+
- ⚠️ YouTube (working sometimes)
|
|
56
59
|
- ⏳ Threads
|
|
57
60
|
- ⏳ 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
|
|
@@ -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.
|
|
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.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -46,14 +46,15 @@ URL (string) --> retrieve() --> MultimodalSequence
|
|
|
46
46
|
```
|
|
47
47
|
The `MultimodalSequence` is a sequence of Markdown-formatted text and media provided by the [ezMM](https://github.com/multimodal-ai-lab/ezmm) library.
|
|
48
48
|
|
|
49
|
-
Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl).
|
|
49
|
+
Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl) and [Decodo](https://decodo.com/).
|
|
50
50
|
|
|
51
51
|
## Supported Proprietary APIs
|
|
52
52
|
- ✅ X/Twitter
|
|
53
53
|
- ✅ Telegram
|
|
54
54
|
- ✅ Bluesky
|
|
55
55
|
- ✅ TikTok
|
|
56
|
+
- ⚠️ Facebook (working only sometimes and only with yt-dlp and Decodo)
|
|
57
|
+
- ⚠️ Instagram (done for videos but not for images yet)
|
|
58
|
+
- ⚠️ YouTube (working sometimes)
|
|
56
59
|
- ⏳ Threads
|
|
57
60
|
- ⏳ 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
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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]:
|