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.
Files changed (39) hide show
  1. {scrapemm-0.2.1 → scrapemm-0.3.0}/.gitignore +2 -1
  2. {scrapemm-0.2.1/scrapeMM.egg-info → scrapemm-0.3.0}/PKG-INFO +5 -4
  3. {scrapemm-0.2.1 → scrapemm-0.3.0}/README.md +4 -3
  4. {scrapemm-0.2.1 → scrapemm-0.3.0}/pyproject.toml +2 -2
  5. {scrapemm-0.2.1 → scrapemm-0.3.0}/requirements.txt +3 -2
  6. {scrapemm-0.2.1 → scrapemm-0.3.0/scrapeMM.egg-info}/PKG-INFO +5 -4
  7. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapeMM.egg-info/SOURCES.txt +5 -0
  8. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/integrations/__init__.py +7 -9
  9. scrapemm-0.3.0/scrapemm/integrations/base.py +39 -0
  10. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/integrations/bluesky.py +7 -11
  11. scrapemm-0.3.0/scrapemm/integrations/fb.py +97 -0
  12. scrapemm-0.3.0/scrapemm/integrations/instagram.py +84 -0
  13. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/integrations/telegram.py +13 -5
  14. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/integrations/tiktok.py +61 -68
  15. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/integrations/x.py +18 -17
  16. scrapemm-0.3.0/scrapemm/integrations/youtube.py +34 -0
  17. scrapemm-0.3.0/scrapemm/retrieval.py +118 -0
  18. scrapemm-0.3.0/scrapemm/scraping/__init__.py +2 -0
  19. scrapemm-0.3.0/scrapemm/scraping/decodo.py +191 -0
  20. scrapemm-0.3.0/scrapemm/scraping/firecrawl.py +96 -0
  21. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/scraping/util.py +20 -12
  22. scrapemm-0.3.0/scrapemm/scraping/ytdlp.py +203 -0
  23. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/secrets.py +27 -11
  24. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/util.py +1 -1
  25. {scrapemm-0.2.1 → scrapemm-0.3.0}/testing/test_retrieval.py +38 -1
  26. scrapemm-0.2.1/scrapemm/integrations/base.py +0 -16
  27. scrapemm-0.2.1/scrapemm/retrieval.py +0 -65
  28. scrapemm-0.2.1/scrapemm/scraping/__init__.py +0 -0
  29. scrapemm-0.2.1/scrapemm/scraping/firecrawl.py +0 -157
  30. {scrapemm-0.2.1 → scrapemm-0.3.0}/LICENSE +0 -0
  31. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapeMM.egg-info/dependency_links.txt +0 -0
  32. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapeMM.egg-info/requires.txt +0 -0
  33. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapeMM.egg-info/top_level.txt +0 -0
  34. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/__init__.py +0 -0
  35. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/common.py +0 -0
  36. {scrapemm-0.2.1 → scrapemm-0.3.0}/scrapemm/scraping/no_bot_domains.txt +0 -0
  37. {scrapemm-0.2.1 → scrapemm-0.3.0}/scripts/example.py +0 -0
  38. {scrapemm-0.2.1 → scrapemm-0.3.0}/setup.cfg +0 -0
  39. {scrapemm-0.2.1 → scrapemm-0.3.0}/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.1
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.2.1"
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>=60", "setuptools-scm>=8.0"]
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.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.1
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
- 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
@@ -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 __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]: