scrapeMM 0.4.0__tar.gz → 0.4.2__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.4.0/scrapeMM.egg-info → scrapemm-0.4.2}/PKG-INFO +14 -5
- {scrapemm-0.4.0 → scrapemm-0.4.2}/README.md +13 -4
- {scrapemm-0.4.0 → scrapemm-0.4.2}/pyproject.toml +7 -1
- {scrapemm-0.4.0 → scrapemm-0.4.2/scrapeMM.egg-info}/PKG-INFO +14 -5
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapeMM.egg-info/SOURCES.txt +1 -0
- scrapemm-0.4.2/scrapemm/common/exceptions.py +8 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/common/scraping_response.py +1 -1
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/base.py +2 -2
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/bluesky.py +13 -3
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/fb.py +9 -10
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/instagram.py +7 -10
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/telegram.py +46 -43
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/tiktok.py +42 -200
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/x.py +7 -2
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/ytdlp.py +30 -15
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/retrieval.py +40 -16
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/util.py +1 -1
- scrapemm-0.4.2/scripts/example.py +10 -0
- scrapemm-0.4.2/testing/test_retrieval.py +100 -0
- scrapemm-0.4.0/scripts/example.py +0 -7
- scrapemm-0.4.0/testing/test_retrieval.py +0 -163
- {scrapemm-0.4.0 → scrapemm-0.4.2}/.gitignore +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/LICENSE +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/requirements.txt +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapeMM.egg-info/dependency_links.txt +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapeMM.egg-info/requires.txt +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapeMM.egg-info/top_level.txt +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/__init__.py +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/common/__init__.py +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/__init__.py +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/decodo.py +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/firecrawl/__init__.py +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/firecrawl/firecrawl.py +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/firecrawl/no_bot_domains.txt +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/integrations/youtube.py +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/scrapemm/secrets.py +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/setup.cfg +0 -0
- {scrapemm-0.4.0 → scrapemm-0.4.2}/testing/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrapeMM
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -50,12 +50,21 @@ The `MultimodalSequence` is a sequence of Markdown-formatted text and media prov
|
|
|
50
50
|
Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl) and [Decodo](https://decodo.com/).
|
|
51
51
|
|
|
52
52
|
## Supported Platforms
|
|
53
|
+
### Social Media
|
|
53
54
|
- ✅ X/Twitter
|
|
54
55
|
- ✅ Telegram
|
|
55
56
|
- ✅ Bluesky
|
|
56
57
|
- ✅ TikTok
|
|
57
58
|
- ✅ YouTube
|
|
58
|
-
- (✅️) Instagram
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
59
|
+
- (✅️) Instagram: works for most content
|
|
60
|
+
- ⏳ Facebook: done for videos but not for images yet
|
|
61
|
+
- ❌ Threads: TBD
|
|
62
|
+
- ❌ Reddit: TBD
|
|
63
|
+
|
|
64
|
+
### Archiving Services
|
|
65
|
+
- ❌ Perma.cc
|
|
66
|
+
- ❌ Archive.today
|
|
67
|
+
- ❌ Wayback Machine, Internet Archive (web.archive.org)
|
|
68
|
+
- ❌ AwesomeScreenshot.com
|
|
69
|
+
- ⏳ MediaVault (mvau.lt): Works for images but not for videos yet
|
|
70
|
+
- ❌ Ghost Archive (ghostarchive.org)
|
|
@@ -26,12 +26,21 @@ The `MultimodalSequence` is a sequence of Markdown-formatted text and media prov
|
|
|
26
26
|
Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl) and [Decodo](https://decodo.com/).
|
|
27
27
|
|
|
28
28
|
## Supported Platforms
|
|
29
|
+
### Social Media
|
|
29
30
|
- ✅ X/Twitter
|
|
30
31
|
- ✅ Telegram
|
|
31
32
|
- ✅ Bluesky
|
|
32
33
|
- ✅ TikTok
|
|
33
34
|
- ✅ YouTube
|
|
34
|
-
- (✅️) Instagram
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
35
|
+
- (✅️) Instagram: works for most content
|
|
36
|
+
- ⏳ Facebook: done for videos but not for images yet
|
|
37
|
+
- ❌ Threads: TBD
|
|
38
|
+
- ❌ Reddit: TBD
|
|
39
|
+
|
|
40
|
+
### Archiving Services
|
|
41
|
+
- ❌ Perma.cc
|
|
42
|
+
- ❌ Archive.today
|
|
43
|
+
- ❌ Wayback Machine, Internet Archive (web.archive.org)
|
|
44
|
+
- ❌ AwesomeScreenshot.com
|
|
45
|
+
- ⏳ MediaVault (mvau.lt): Works for images but not for videos yet
|
|
46
|
+
- ❌ Ghost Archive (ghostarchive.org)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "scrapeMM"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.2"
|
|
4
4
|
description = "Flexible multimodal scraper for social media and the open web."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -33,3 +33,9 @@ namespaces = false # Disable implicit namespaces
|
|
|
33
33
|
|
|
34
34
|
[tool.setuptools.package-data]
|
|
35
35
|
"*" = ["*.txt"] # Include all .txt files contained in any (sub)package in the build
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = [
|
|
39
|
+
"testing/",
|
|
40
|
+
]
|
|
41
|
+
asyncio_default_fixture_loop_scope = "session"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrapeMM
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -50,12 +50,21 @@ The `MultimodalSequence` is a sequence of Markdown-formatted text and media prov
|
|
|
50
50
|
Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl) and [Decodo](https://decodo.com/).
|
|
51
51
|
|
|
52
52
|
## Supported Platforms
|
|
53
|
+
### Social Media
|
|
53
54
|
- ✅ X/Twitter
|
|
54
55
|
- ✅ Telegram
|
|
55
56
|
- ✅ Bluesky
|
|
56
57
|
- ✅ TikTok
|
|
57
58
|
- ✅ YouTube
|
|
58
|
-
- (✅️) Instagram
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
59
|
+
- (✅️) Instagram: works for most content
|
|
60
|
+
- ⏳ Facebook: done for videos but not for images yet
|
|
61
|
+
- ❌ Threads: TBD
|
|
62
|
+
- ❌ Reddit: TBD
|
|
63
|
+
|
|
64
|
+
### Archiving Services
|
|
65
|
+
- ❌ Perma.cc
|
|
66
|
+
- ❌ Archive.today
|
|
67
|
+
- ❌ Wayback Machine, Internet Archive (web.archive.org)
|
|
68
|
+
- ❌ AwesomeScreenshot.com
|
|
69
|
+
- ⏳ MediaVault (mvau.lt): Works for images but not for videos yet
|
|
70
|
+
- ❌ Ghost Archive (ghostarchive.org)
|
|
@@ -8,7 +8,7 @@ from ezmm import MultimodalSequence
|
|
|
8
8
|
class ScrapingResponse:
|
|
9
9
|
url: str # The input URL that was scraped
|
|
10
10
|
content: Optional[MultimodalSequence | str] # The retrieved content. None if retrieval failed.
|
|
11
|
-
errors: dict[str, Optional[Exception]] = None # The exceptions raised during retrieval for each method
|
|
11
|
+
errors: dict[str, Optional[Exception]] | None = None # The exceptions raised during retrieval for each method
|
|
12
12
|
method: Optional[str] = None # The successful method used to retrieve the content
|
|
13
13
|
|
|
14
14
|
@property
|
|
@@ -15,7 +15,7 @@ class RetrievalIntegration(ABC):
|
|
|
15
15
|
|
|
16
16
|
name: str
|
|
17
17
|
domains: list[str] # The domains supported by this integration
|
|
18
|
-
connected: bool = None
|
|
18
|
+
connected: bool | None = None
|
|
19
19
|
|
|
20
20
|
@abstractmethod
|
|
21
21
|
async def _connect(self):
|
|
@@ -25,7 +25,7 @@ class RetrievalIntegration(ABC):
|
|
|
25
25
|
|
|
26
26
|
async def get(self, url: str, **kwargs) -> Optional[MultimodalSequence]:
|
|
27
27
|
"""Ensures connectivity before invoking the service for retrieval."""
|
|
28
|
-
assert get_domain(url) in self.domains
|
|
28
|
+
assert get_domain(url) in self.domains, f"Invalid domain {get_domain(url)} for integration {self.name}."
|
|
29
29
|
if self.connected is None:
|
|
30
30
|
await self._connect()
|
|
31
31
|
if not self.connected:
|
|
@@ -34,14 +34,20 @@ class Bluesky(RetrievalIntegration):
|
|
|
34
34
|
return None
|
|
35
35
|
|
|
36
36
|
session = kwargs.get("session")
|
|
37
|
+
max_video_size = kwargs.get("max_video_size")
|
|
37
38
|
if "post" in url:
|
|
38
|
-
result = await self._retrieve_post(url, session)
|
|
39
|
+
result = await self._retrieve_post(url, session, max_video_size)
|
|
39
40
|
else:
|
|
40
41
|
result = await self._retrieve_profile(url, session)
|
|
41
42
|
|
|
42
43
|
return result
|
|
43
44
|
|
|
44
|
-
async def _retrieve_post(
|
|
45
|
+
async def _retrieve_post(
|
|
46
|
+
self,
|
|
47
|
+
url: str,
|
|
48
|
+
session: aiohttp.ClientSession | None = None,
|
|
49
|
+
max_video_size: int | None = None
|
|
50
|
+
) -> Optional[MultimodalSequence]:
|
|
45
51
|
"""Retrieve a post from the given Bluesky URL."""
|
|
46
52
|
uri = self._construct_uri(url)
|
|
47
53
|
if not uri:
|
|
@@ -94,7 +100,11 @@ class Bluesky(RetrievalIntegration):
|
|
|
94
100
|
elif hasattr(embed, 'py_type') and getattr(embed, 'py_type') == 'app.bsky.embed.video#view':
|
|
95
101
|
video = await download_video(embed.playlist, session)
|
|
96
102
|
if video:
|
|
97
|
-
|
|
103
|
+
if max_video_size is None or video.size <= max_video_size:
|
|
104
|
+
media.append(video)
|
|
105
|
+
else:
|
|
106
|
+
logger.info(f"Removing video {video.reference} because it exceeds the maximum size "
|
|
107
|
+
f"of {max_video_size / 1024 / 1024:.2f} MB.")
|
|
98
108
|
|
|
99
109
|
# Extract hashtags and mentions
|
|
100
110
|
hashtags, mentions, external_links = [], [], []
|
|
@@ -15,7 +15,7 @@ VIDEO_URL_REGEX = r"facebook\.com/\d+/videos/\d+/?"
|
|
|
15
15
|
|
|
16
16
|
class Facebook(RetrievalIntegration):
|
|
17
17
|
name = "Facebook"
|
|
18
|
-
domains = ["facebook.com", "
|
|
18
|
+
domains = ["facebook.com", "fb.watch"]
|
|
19
19
|
|
|
20
20
|
async def _connect(self):
|
|
21
21
|
self.api_available = False # TODO
|
|
@@ -24,10 +24,6 @@ class Facebook(RetrievalIntegration):
|
|
|
24
24
|
|
|
25
25
|
async def _get(self, url: str, **kwargs) -> MultimodalSequence | None:
|
|
26
26
|
"""Retrieves content from a Facebook post URL."""
|
|
27
|
-
if get_domain(url) not in self.domains:
|
|
28
|
-
logger.error(f"❌ Invalid domain for Facebook: {get_domain(url)}")
|
|
29
|
-
return None
|
|
30
|
-
|
|
31
27
|
# Determine if this is a video or photo URL, act accordingly
|
|
32
28
|
if self._is_video_url(url):
|
|
33
29
|
return await self._get_video(url, **kwargs)
|
|
@@ -42,23 +38,26 @@ class Facebook(RetrievalIntegration):
|
|
|
42
38
|
async def _get_video(self, url: str, **kwargs) -> MultimodalSequence | None:
|
|
43
39
|
"""Retrieves content from a Facebook video URL."""
|
|
44
40
|
if self.api_available:
|
|
45
|
-
raise NotImplementedError("
|
|
41
|
+
raise NotImplementedError("Facebook video retrieval through API not yet supported.")
|
|
46
42
|
else:
|
|
47
43
|
return await get_content_with_ytdlp(url, platform="Facebook", **kwargs)
|
|
48
44
|
|
|
49
45
|
async def _get_photo(self, url: str, **kwargs) -> MultimodalSequence | None:
|
|
50
46
|
"""Retrieves content from a Facebook photo URL."""
|
|
51
|
-
raise NotImplementedError("
|
|
47
|
+
raise NotImplementedError("No available method to retrieve Facebook photos.")
|
|
52
48
|
|
|
53
49
|
async def _get_user_profile(self, url: str, **kwargs) -> MultimodalSequence | None:
|
|
54
50
|
"""Retrieves content from a Facebook user profile URL."""
|
|
55
|
-
raise NotImplementedError("
|
|
51
|
+
raise NotImplementedError("No available method to retrieve Facebook profiles.")
|
|
56
52
|
|
|
57
53
|
def _is_video_url(self, url: str) -> bool:
|
|
58
54
|
"""Checks if the URL is a Facebook video URL."""
|
|
59
|
-
# video URLS are in the format: https://www.facebook.com/watch?v=VIDEO_ID
|
|
55
|
+
# video URLS are in the format: https://www.facebook.com/watch?v=VIDEO_ID or fb.watch/...
|
|
60
56
|
# or Reels: https://www.facebook.com/reel/REEL_ID
|
|
61
|
-
return "facebook.com/watch" in url
|
|
57
|
+
return ("facebook.com/watch" in url
|
|
58
|
+
or "facebook.com/reel" in url
|
|
59
|
+
or bool(re.search(VIDEO_URL_REGEX, url))
|
|
60
|
+
or "fb.watch" in url)
|
|
62
61
|
|
|
63
62
|
def _extract_video_id(self, url: str) -> str:
|
|
64
63
|
"""Extracts the video ID from a Facebook video URL."""
|
|
@@ -4,6 +4,7 @@ from urllib.parse import urlparse
|
|
|
4
4
|
from ezmm import MultimodalSequence
|
|
5
5
|
from yt_dlp import DownloadError
|
|
6
6
|
|
|
7
|
+
from scrapemm.common.exceptions import RateLimitError
|
|
7
8
|
from scrapemm.integrations.base import RetrievalIntegration
|
|
8
9
|
from scrapemm.integrations.ytdlp import get_content_with_ytdlp
|
|
9
10
|
from scrapemm.util import get_domain
|
|
@@ -22,16 +23,15 @@ class Instagram(RetrievalIntegration):
|
|
|
22
23
|
|
|
23
24
|
async def _get(self, url: str, **kwargs) -> MultimodalSequence | None:
|
|
24
25
|
"""Retrieves content from an Instagram post URL."""
|
|
25
|
-
if get_domain(url) not in self.domains:
|
|
26
|
-
logger.error(f"❌ Invalid domain for Instagram: {get_domain(url)}")
|
|
27
|
-
return None
|
|
28
|
-
|
|
29
26
|
# Determine if this is a video or profile URL
|
|
30
27
|
if self._is_video_url(url):
|
|
31
28
|
return await self._get_video(url, **kwargs)
|
|
32
29
|
elif self._is_photo_url(url):
|
|
33
30
|
# /p/ URLs can also be reels, so try both
|
|
34
|
-
|
|
31
|
+
try:
|
|
32
|
+
content = await self._get_video(url, **kwargs)
|
|
33
|
+
except Exception:
|
|
34
|
+
content = None
|
|
35
35
|
if content and content.has_videos():
|
|
36
36
|
return content
|
|
37
37
|
else:
|
|
@@ -48,16 +48,13 @@ class Instagram(RetrievalIntegration):
|
|
|
48
48
|
return await get_content_with_ytdlp(url, platform="Instagram", **kwargs)
|
|
49
49
|
except DownloadError as e:
|
|
50
50
|
if "rate-limit reached" in str(e):
|
|
51
|
-
|
|
52
|
-
logger.error(msg)
|
|
53
|
-
raise RuntimeError(msg) from e
|
|
51
|
+
raise RateLimitError(f"Instagram rate limit likely reached: {e}")
|
|
54
52
|
else:
|
|
55
53
|
raise e
|
|
56
54
|
|
|
57
55
|
async def _get_photo(self, url: str, **kwargs) -> MultimodalSequence | None:
|
|
58
56
|
"""Retrieves content from an Instagram photo URL (can also be a reel)."""
|
|
59
|
-
|
|
60
|
-
return None
|
|
57
|
+
raise NotImplementedError("Native Instagram photo download not yet supported. Use Decodo for that.")
|
|
61
58
|
|
|
62
59
|
async def _get_user_profile(self, url: str, **kwargs) -> MultimodalSequence | None:
|
|
63
60
|
"""Retrieves content from an Instagram user profile URL."""
|
|
@@ -31,11 +31,12 @@ class Telegram(RetrievalIntegration):
|
|
|
31
31
|
if api_id and api_hash and bot_token:
|
|
32
32
|
self.client = TelegramClient(self.session_path, api_id, api_hash)
|
|
33
33
|
try:
|
|
34
|
-
self.client.start(bot_token=bot_token)
|
|
34
|
+
await self.client.start(bot_token=bot_token) # Returns a coroutine b/c event loop exists already
|
|
35
35
|
except sqlite3.OperationalError: # Database is locked from an interrupted previous session
|
|
36
36
|
# Remove the database file and try again
|
|
37
37
|
journal_path = Path(self.session_path + ".session-journal")
|
|
38
38
|
journal_path.unlink(missing_ok=True)
|
|
39
|
+
await self.client.start(bot_token=bot_token) # Returns a coroutine b/c event loop exists already
|
|
39
40
|
self.connected = True
|
|
40
41
|
logger.info("✅ Successfully connected to Telegram.")
|
|
41
42
|
else:
|
|
@@ -44,8 +45,6 @@ class Telegram(RetrievalIntegration):
|
|
|
44
45
|
|
|
45
46
|
async def _get(self, url: str, **kwargs) -> Optional[MultimodalSequence]:
|
|
46
47
|
"""Retrieves content from a Telegram post URL."""
|
|
47
|
-
assert get_domain(url) in self.domains
|
|
48
|
-
|
|
49
48
|
# Parse the URL to get channel/group name and post ID
|
|
50
49
|
parsed = urlparse(url)
|
|
51
50
|
path_parts = parsed.path.strip("/").split("/")
|
|
@@ -56,40 +55,44 @@ class Telegram(RetrievalIntegration):
|
|
|
56
55
|
channel_name = path_parts[0]
|
|
57
56
|
post_id = int(path_parts[1])
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
name
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
name
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
58
|
+
# Get the message
|
|
59
|
+
channel = await self.client.get_entity(channel_name)
|
|
60
|
+
if not channel:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
assert isinstance(channel, Channel), f"Can only retrieve posts from Telegram channels, but got {type(channel).__name__}."
|
|
64
|
+
|
|
65
|
+
message = await self.client.get_messages(channel, ids=post_id)
|
|
66
|
+
if not message:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
# Handle media
|
|
70
|
+
max_video_size = kwargs.get("max_video_size")
|
|
71
|
+
media = await self._get_media_from_message(channel, message, max_video_size=max_video_size)
|
|
72
|
+
|
|
73
|
+
author = message.sender
|
|
74
|
+
author_type = type(author).__name__
|
|
75
|
+
if isinstance(author, Channel):
|
|
76
|
+
name = f'"{author.title}"'
|
|
77
|
+
if author.username:
|
|
78
|
+
name += f" (@{author.username})"
|
|
79
|
+
elif isinstance(author, User):
|
|
80
|
+
if author.bot:
|
|
81
|
+
author_type = "Bot"
|
|
82
|
+
name = f"{author.first_name} {author.last_name}" if author.last_name else author.first_name
|
|
83
|
+
if author.username:
|
|
84
|
+
name += f" (@{author.username})"
|
|
85
|
+
if author.phone:
|
|
86
|
+
name += f", Phone: {author.phone}"
|
|
87
|
+
if author.verified:
|
|
88
|
+
name += " (Verified)"
|
|
89
|
+
else:
|
|
90
|
+
name = "Unknown"
|
|
91
|
+
|
|
92
|
+
edit_text = "\nEdit date: " + message.edit_date.strftime("%B %d, %Y at %H:%M") if message.edit_date else ""
|
|
93
|
+
reactions_text = "\nReactions: " + message.reactions.stringify() if message.reactions else ""
|
|
94
|
+
|
|
95
|
+
text = f"""**Telegram Post**
|
|
93
96
|
Author: {author_type} {name}
|
|
94
97
|
Date: {message.date.strftime("%B %d, %Y at %H:%M")}{edit_text}
|
|
95
98
|
Views: {message.views}
|
|
@@ -98,13 +101,9 @@ Forwards: {message.forwards}{reactions_text}
|
|
|
98
101
|
{' '.join(m.reference for m in media)}
|
|
99
102
|
{message.text}"""
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
except Exception as e:
|
|
104
|
-
logger.warning(f"Error retrieving Telegram content: {e}")
|
|
105
|
-
return None
|
|
104
|
+
return MultimodalSequence(text)
|
|
106
105
|
|
|
107
|
-
async def _get_media_from_message(self, chat, original_post, max_amp=10) -> list[Item]:
|
|
106
|
+
async def _get_media_from_message(self, chat, original_post, max_amp=10, max_video_size=None) -> list[Item]:
|
|
108
107
|
"""
|
|
109
108
|
Searches for Telegram posts that are part of the same group of uploads.
|
|
110
109
|
The search is conducted around the id of the original post with an amplitude
|
|
@@ -129,6 +128,10 @@ Forwards: {message.forwards}{reactions_text}
|
|
|
129
128
|
item = Image(binary_data=medium_bytes, source_url=post_url)
|
|
130
129
|
elif hasattr(medium, "video"):
|
|
131
130
|
item = Video(binary_data=medium_bytes, source_url=post_url)
|
|
131
|
+
if max_video_size is not None and item.size > max_video_size:
|
|
132
|
+
logger.info(f"Removing video {item.reference} because it exceeds the maximum size "
|
|
133
|
+
f"of {max_video_size / 1024 / 1024:.2f} MB.")
|
|
134
|
+
item = None
|
|
132
135
|
else:
|
|
133
136
|
raise ValueError(f"Unsupported medium: {medium.__dict__}")
|
|
134
137
|
media.append(item)
|