scrapeMM 0.2.1__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.1 → scrapemm-0.3.0}/.gitignore +2 -1
- {scrapemm-0.2.1/scrapeMM.egg-info → scrapemm-0.3.0}/PKG-INFO +5 -4
- {scrapemm-0.2.1 → scrapemm-0.3.0}/README.md +4 -3
- {scrapemm-0.2.1 → scrapemm-0.3.0}/pyproject.toml +2 -2
- {scrapemm-0.2.1 → scrapemm-0.3.0}/requirements.txt +3 -2
- {scrapemm-0.2.1 → scrapemm-0.3.0/scrapeMM.egg-info}/PKG-INFO +5 -4
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapeMM.egg-info/SOURCES.txt +5 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/integrations/__init__.py +7 -9
- scrapemm-0.3.0/scrapemm/integrations/base.py +39 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/integrations/bluesky.py +7 -11
- scrapemm-0.3.0/scrapemm/integrations/fb.py +97 -0
- scrapemm-0.3.0/scrapemm/integrations/instagram.py +84 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/integrations/telegram.py +13 -5
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/integrations/tiktok.py +61 -68
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/integrations/x.py +18 -17
- 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.1 → scrapemm-0.3.0}/scrapemm/scraping/util.py +20 -12
- scrapemm-0.3.0/scrapemm/scraping/ytdlp.py +203 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/secrets.py +27 -11
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/util.py +1 -1
- {scrapemm-0.2.1 → scrapemm-0.3.0}/testing/test_retrieval.py +38 -1
- scrapemm-0.2.1/scrapemm/integrations/base.py +0 -16
- scrapemm-0.2.1/scrapemm/retrieval.py +0 -65
- scrapemm-0.2.1/scrapemm/scraping/__init__.py +0 -0
- scrapemm-0.2.1/scrapemm/scraping/firecrawl.py +0 -157
- {scrapemm-0.2.1 → scrapemm-0.3.0}/LICENSE +0 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapeMM.egg-info/dependency_links.txt +0 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapeMM.egg-info/requires.txt +0 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapeMM.egg-info/top_level.txt +0 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/__init__.py +0 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/common.py +0 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/scraping/no_bot_domains.txt +0 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/scripts/example.py +0 -0
- {scrapemm-0.2.1 → scrapemm-0.3.0}/setup.cfg +0 -0
- {scrapemm-0.2.1 → 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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "scrapeMM"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
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"
|
|
@@ -24,7 +24,7 @@ Homepage = "https://github.com/multimodal-ai-lab/scrapeMM"
|
|
|
24
24
|
Issues = "https://github.com/multimodal-ai-lab/scrapeMM"
|
|
25
25
|
|
|
26
26
|
[build-system]
|
|
27
|
-
requires = ["setuptools
|
|
27
|
+
requires = ["setuptools"]
|
|
28
28
|
build-backend = "setuptools.build_meta"
|
|
29
29
|
|
|
30
30
|
[tool.setuptools.packages.find]
|
|
@@ -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
|
|
@@ -16,13 +16,18 @@ scrapemm/util.py
|
|
|
16
16
|
scrapemm/integrations/__init__.py
|
|
17
17
|
scrapemm/integrations/base.py
|
|
18
18
|
scrapemm/integrations/bluesky.py
|
|
19
|
+
scrapemm/integrations/fb.py
|
|
20
|
+
scrapemm/integrations/instagram.py
|
|
19
21
|
scrapemm/integrations/telegram.py
|
|
20
22
|
scrapemm/integrations/tiktok.py
|
|
21
23
|
scrapemm/integrations/x.py
|
|
24
|
+
scrapemm/integrations/youtube.py
|
|
22
25
|
scrapemm/scraping/__init__.py
|
|
26
|
+
scrapemm/scraping/decodo.py
|
|
23
27
|
scrapemm/scraping/firecrawl.py
|
|
24
28
|
scrapemm/scraping/no_bot_domains.txt
|
|
25
29
|
scrapemm/scraping/util.py
|
|
30
|
+
scrapemm/scraping/ytdlp.py
|
|
26
31
|
scripts/example.py
|
|
27
32
|
testing/test_retrieval.py
|
|
28
33
|
testing/test_utils.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
|
|
@@ -10,19 +5,22 @@ from ezmm import MultimodalSequence
|
|
|
10
5
|
|
|
11
6
|
from scrapemm.util import get_domain
|
|
12
7
|
from .bluesky import Bluesky
|
|
8
|
+
from .fb import Facebook
|
|
9
|
+
from .instagram import Instagram
|
|
13
10
|
from .telegram import Telegram
|
|
14
11
|
from .tiktok import TikTok
|
|
15
12
|
from .x import X
|
|
13
|
+
from .youtube import YouTube
|
|
16
14
|
|
|
17
|
-
RETRIEVAL_INTEGRATIONS = [X(), Telegram(), Bluesky(), TikTok()]
|
|
15
|
+
RETRIEVAL_INTEGRATIONS = [X(), Telegram(), Bluesky(), TikTok(), Instagram(), Facebook(), YouTube()]
|
|
18
16
|
DOMAIN_TO_INTEGRATION = {domain: integration
|
|
19
17
|
for integration in RETRIEVAL_INTEGRATIONS
|
|
20
|
-
for domain in integration.domains
|
|
21
|
-
if integration.connected}
|
|
18
|
+
for domain in integration.domains}
|
|
22
19
|
|
|
23
20
|
|
|
24
21
|
async def retrieve_via_integration(url: str, session: aiohttp.ClientSession) -> Optional[MultimodalSequence]:
|
|
25
22
|
domain = get_domain(url)
|
|
26
23
|
if domain in DOMAIN_TO_INTEGRATION:
|
|
27
24
|
integration = DOMAIN_TO_INTEGRATION[domain]
|
|
28
|
-
|
|
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
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
import aiohttp
|
|
4
|
+
from ezmm import MultimodalSequence, download_image
|
|
5
|
+
|
|
6
|
+
from scrapemm.scraping.ytdlp import check_ytdlp_available, get_video_with_ytdlp
|
|
7
|
+
from scrapemm.integrations.base import RetrievalIntegration
|
|
8
|
+
from scrapemm.util import get_domain
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("scrapeMM")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Facebook(RetrievalIntegration):
|
|
14
|
+
name = "Facebook"
|
|
15
|
+
domains = ["facebook.com", "www.facebook.com"]
|
|
16
|
+
|
|
17
|
+
async def _connect(self):
|
|
18
|
+
# Check if yt-dlp is available
|
|
19
|
+
self.ytdlp_available = check_ytdlp_available()
|
|
20
|
+
|
|
21
|
+
if self.ytdlp_available:
|
|
22
|
+
self.connected = True
|
|
23
|
+
mode = "yt-dlp only"
|
|
24
|
+
logger.info(f"✅ Facebook integration ready ({mode} mode).")
|
|
25
|
+
else:
|
|
26
|
+
self.connected = False
|
|
27
|
+
logger.warning("❌ Facebook integration not available: Neither API credentials nor yt-dlp found.")
|
|
28
|
+
|
|
29
|
+
async def _get(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
|
|
30
|
+
"""Retrieves content from a Facebook post URL."""
|
|
31
|
+
if get_domain(url) not in self.domains:
|
|
32
|
+
logger.error(f"❌ Invalid domain for Facebook: {get_domain(url)}")
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
# Determine if this is a video or profile URL
|
|
36
|
+
if self._is_video_url(url):
|
|
37
|
+
return await self._get_video(url, session)
|
|
38
|
+
elif self._is_photo_url(url):
|
|
39
|
+
return await self._get_photo(url, session)
|
|
40
|
+
else:
|
|
41
|
+
return await self._get_user_profile(url, session)
|
|
42
|
+
|
|
43
|
+
async def _get_video(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
|
|
44
|
+
"""Retrieves content from a TikTok video URL."""
|
|
45
|
+
|
|
46
|
+
# Fallback to yt-dlp mode
|
|
47
|
+
if self.ytdlp_available:
|
|
48
|
+
return await get_video_with_ytdlp(url, session, platform="Facebook")
|
|
49
|
+
|
|
50
|
+
logger.error("❌ No available method to retrieve Facebook video.")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
async def _get_photo(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
|
|
54
|
+
"""Retrieves content from a Facebook photo URL."""
|
|
55
|
+
logger.error("❌ No available method to retrieve Facebook photo.")
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
async def _get_user_profile(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
|
|
59
|
+
"""Retrieves content from a Facebook user profile URL."""
|
|
60
|
+
username = self._extract_username(url)
|
|
61
|
+
if username:
|
|
62
|
+
text = f"""**Facebook Profile**
|
|
63
|
+
Username: @{username}
|
|
64
|
+
URL: {url}
|
|
65
|
+
|
|
66
|
+
Note: Profile details require Facebook API access.
|
|
67
|
+
Configure API credentials for full profile information."""
|
|
68
|
+
return MultimodalSequence([text])
|
|
69
|
+
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def _is_video_url(self, url: str) -> bool:
|
|
73
|
+
"""Checks if the URL is a Facebook video URL."""
|
|
74
|
+
# video URLS are in the format: https://www.facebook.com/watch?v=VIDEO_ID
|
|
75
|
+
return "facebook.com/watch" in url
|
|
76
|
+
|
|
77
|
+
def _extract_video_id(self, url: str) -> str:
|
|
78
|
+
"""Extracts the video ID from a Facebook video URL."""
|
|
79
|
+
parsed_url = urlparse(url)
|
|
80
|
+
query_params = parsed_url.query
|
|
81
|
+
for param in query_params.split('&'):
|
|
82
|
+
if param.startswith('v='):
|
|
83
|
+
return param.split('=')[1]
|
|
84
|
+
return ""
|
|
85
|
+
|
|
86
|
+
def _is_photo_url(self, url: str) -> bool:
|
|
87
|
+
"""Checks if the URL is a Facebook photo URL."""
|
|
88
|
+
return "facebook.com/photo" in url or "facebook.com/photos" in url
|
|
89
|
+
|
|
90
|
+
def _extract_username(self, url: str) -> str:
|
|
91
|
+
"""Extracts the username from a Facebook profile URL."""
|
|
92
|
+
# url format: https://www.facebook.com/username<?...>
|
|
93
|
+
parsed_url = urlparse(url)
|
|
94
|
+
path_parts = parsed_url.path.strip('/').split('/')
|
|
95
|
+
if len(path_parts) > 0:
|
|
96
|
+
return path_parts[0]
|
|
97
|
+
return ""
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
import aiohttp
|
|
4
|
+
from ezmm import MultimodalSequence, download_image
|
|
5
|
+
|
|
6
|
+
from scrapemm.scraping.ytdlp import check_ytdlp_available, get_video_with_ytdlp
|
|
7
|
+
from scrapemm.integrations.base import RetrievalIntegration
|
|
8
|
+
from scrapemm.util import get_domain
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("scrapeMM")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Instagram(RetrievalIntegration):
|
|
14
|
+
name = "Instagram"
|
|
15
|
+
domains = ["instagram.com", "www.instagram.com"]
|
|
16
|
+
|
|
17
|
+
async def _connect(self):
|
|
18
|
+
# Check if yt-dlp is available
|
|
19
|
+
self.ytdlp_available = check_ytdlp_available()
|
|
20
|
+
|
|
21
|
+
if self.ytdlp_available:
|
|
22
|
+
self.connected = True
|
|
23
|
+
mode = "yt-dlp only"
|
|
24
|
+
logger.info(f"✅ Instagram integration ready ({mode} mode).")
|
|
25
|
+
else:
|
|
26
|
+
self.connected = False
|
|
27
|
+
logger.warning("❌ Instagram integration not available: Neither API credentials nor yt-dlp found.")
|
|
28
|
+
|
|
29
|
+
async def _get(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
|
|
30
|
+
"""Retrieves content from an Instagram post URL."""
|
|
31
|
+
if get_domain(url) not in self.domains:
|
|
32
|
+
logger.error(f"❌ Invalid domain for Instagram: {get_domain(url)}")
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
# Determine if this is a video or profile URL
|
|
36
|
+
if self._is_video_url(url):
|
|
37
|
+
return await self._get_video(url, session)
|
|
38
|
+
elif self._is_photo_url(url):
|
|
39
|
+
return await self._get_photo(url, session)
|
|
40
|
+
else:
|
|
41
|
+
return await self._get_user_profile(url, session)
|
|
42
|
+
|
|
43
|
+
async def _get_video(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
|
|
44
|
+
"""Retrieves content from an Instagram video URL."""
|
|
45
|
+
if self.ytdlp_available:
|
|
46
|
+
return await get_video_with_ytdlp(url, session, platform="Instagram")
|
|
47
|
+
|
|
48
|
+
logger.error("❌ No available method to retrieve Instagram video.")
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
async def _get_photo(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
|
|
52
|
+
"""Retrieves content from an Instagram photo URL."""
|
|
53
|
+
logger.error("❌ No available method to retrieve Instagram photo.")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
async def _get_user_profile(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence | None:
|
|
57
|
+
"""Retrieves content from an Instagram user profile URL."""
|
|
58
|
+
username = self._extract_username(url)
|
|
59
|
+
if username:
|
|
60
|
+
text = f"""**Instagram Profile**
|
|
61
|
+
Username: @{username}
|
|
62
|
+
URL: {url}
|
|
63
|
+
|
|
64
|
+
Note: Profile details require Instagram API access.
|
|
65
|
+
Configure API credentials for full profile information."""
|
|
66
|
+
return MultimodalSequence([text])
|
|
67
|
+
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def _is_video_url(self, url: str) -> bool:
|
|
71
|
+
"""Checks if the URL is an Instagram video URL."""
|
|
72
|
+
return "instagram.com/reels" in url or "instagram.com/reel/" in url
|
|
73
|
+
|
|
74
|
+
def _is_photo_url(self, url: str) -> bool:
|
|
75
|
+
"""Checks if the URL is an Instagram photo URL."""
|
|
76
|
+
return "instagram.com/p/" in url
|
|
77
|
+
|
|
78
|
+
def _extract_username(self, url: str) -> str:
|
|
79
|
+
"""Extracts the username from an Instagram profile URL."""
|
|
80
|
+
parsed_url = urlparse(url)
|
|
81
|
+
path_parts = parsed_url.path.strip('/').split('/')
|
|
82
|
+
if len(path_parts) > 0:
|
|
83
|
+
return path_parts[0]
|
|
84
|
+
return ""
|
|
@@ -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]:
|