scrapeMM 0.4.1__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.1/scrapeMM.egg-info → scrapemm-0.4.2}/PKG-INFO +1 -1
- {scrapemm-0.4.1 → scrapemm-0.4.2}/pyproject.toml +7 -1
- {scrapemm-0.4.1 → scrapemm-0.4.2/scrapeMM.egg-info}/PKG-INFO +1 -1
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/common/scraping_response.py +1 -1
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/base.py +1 -1
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/bluesky.py +6 -1
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/fb.py +6 -3
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/instagram.py +5 -3
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/telegram.py +7 -2
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/ytdlp.py +7 -3
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/retrieval.py +29 -13
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scripts/example.py +1 -2
- scrapemm-0.4.2/testing/test_retrieval.py +100 -0
- scrapemm-0.4.1/testing/test_retrieval.py +0 -163
- {scrapemm-0.4.1 → scrapemm-0.4.2}/.gitignore +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/LICENSE +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/README.md +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/requirements.txt +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapeMM.egg-info/SOURCES.txt +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapeMM.egg-info/dependency_links.txt +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapeMM.egg-info/requires.txt +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapeMM.egg-info/top_level.txt +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/__init__.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/common/__init__.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/common/exceptions.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/__init__.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/decodo.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/firecrawl/__init__.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/firecrawl/firecrawl.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/firecrawl/no_bot_domains.txt +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/tiktok.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/x.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/youtube.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/secrets.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/util.py +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/setup.cfg +0 -0
- {scrapemm-0.4.1 → scrapemm-0.4.2}/testing/test_utils.py +0 -0
|
@@ -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"
|
|
@@ -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
|
|
@@ -42,7 +42,12 @@ class Bluesky(RetrievalIntegration):
|
|
|
42
42
|
|
|
43
43
|
return result
|
|
44
44
|
|
|
45
|
-
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]:
|
|
46
51
|
"""Retrieve a post from the given Bluesky URL."""
|
|
47
52
|
uri = self._construct_uri(url)
|
|
48
53
|
if not uri:
|
|
@@ -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
|
|
@@ -52,9 +52,12 @@ class Facebook(RetrievalIntegration):
|
|
|
52
52
|
|
|
53
53
|
def _is_video_url(self, url: str) -> bool:
|
|
54
54
|
"""Checks if the URL is a Facebook video URL."""
|
|
55
|
-
# 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/...
|
|
56
56
|
# or Reels: https://www.facebook.com/reel/REEL_ID
|
|
57
|
-
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)
|
|
58
61
|
|
|
59
62
|
def _extract_video_id(self, url: str) -> str:
|
|
60
63
|
"""Extracts the video ID from a Facebook video URL."""
|
|
@@ -28,7 +28,10 @@ class Instagram(RetrievalIntegration):
|
|
|
28
28
|
return await self._get_video(url, **kwargs)
|
|
29
29
|
elif self._is_photo_url(url):
|
|
30
30
|
# /p/ URLs can also be reels, so try both
|
|
31
|
-
|
|
31
|
+
try:
|
|
32
|
+
content = await self._get_video(url, **kwargs)
|
|
33
|
+
except Exception:
|
|
34
|
+
content = None
|
|
32
35
|
if content and content.has_videos():
|
|
33
36
|
return content
|
|
34
37
|
else:
|
|
@@ -51,8 +54,7 @@ class Instagram(RetrievalIntegration):
|
|
|
51
54
|
|
|
52
55
|
async def _get_photo(self, url: str, **kwargs) -> MultimodalSequence | None:
|
|
53
56
|
"""Retrieves content from an Instagram photo URL (can also be a reel)."""
|
|
54
|
-
|
|
55
|
-
return None
|
|
57
|
+
raise NotImplementedError("Native Instagram photo download not yet supported. Use Decodo for that.")
|
|
56
58
|
|
|
57
59
|
async def _get_user_profile(self, url: str, **kwargs) -> MultimodalSequence | None:
|
|
58
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:
|
|
@@ -56,8 +57,12 @@ class Telegram(RetrievalIntegration):
|
|
|
56
57
|
|
|
57
58
|
# Get the message
|
|
58
59
|
channel = await self.client.get_entity(channel_name)
|
|
59
|
-
|
|
60
|
+
if not channel:
|
|
61
|
+
return None
|
|
60
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)
|
|
61
66
|
if not message:
|
|
62
67
|
return None
|
|
63
68
|
|
|
@@ -10,7 +10,7 @@ import aiohttp
|
|
|
10
10
|
from ezmm import MultimodalSequence, download_image, Video, Image
|
|
11
11
|
from yt_dlp import YoutubeDL
|
|
12
12
|
|
|
13
|
-
from scrapemm.common.exceptions import IPBannedError
|
|
13
|
+
from scrapemm.common.exceptions import IPBannedError, ContentNotFoundError
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger("scrapeMM")
|
|
16
16
|
|
|
@@ -23,8 +23,8 @@ logger_yt_dlp.addHandler(logging.StreamHandler(sys.stdout))
|
|
|
23
23
|
async def download_video_with_ytdlp(
|
|
24
24
|
url: str,
|
|
25
25
|
session: aiohttp.ClientSession,
|
|
26
|
-
max_video_size: int = None,
|
|
27
|
-
cookie_file: str = None
|
|
26
|
+
max_video_size: int | None = None,
|
|
27
|
+
cookie_file: str | None = None
|
|
28
28
|
) -> tuple[Optional[Video], Optional[Image], Optional[dict[str, Any]]]:
|
|
29
29
|
"""Downloads a video and (if not available or exceeds max. duration) its thumbnail, and the metadata using yt-dlp.
|
|
30
30
|
@param max_video_size: Maximum video size in bytes. If the video is larger, the download will be aborted."""
|
|
@@ -79,6 +79,10 @@ async def download_video_with_ytdlp(
|
|
|
79
79
|
if "The following content is not available on this app" in str(e):
|
|
80
80
|
logger.warning(f"You should update yt-dlp to re-enable YouTube downloads.")
|
|
81
81
|
raise e
|
|
82
|
+
elif "Video unavailable" in str(e):
|
|
83
|
+
raise ContentNotFoundError(f"Video is unavailable.")
|
|
84
|
+
elif "Cannot parse data; please report this issue" in str(e):
|
|
85
|
+
raise RuntimeError(f"yt-dlp is unable to parse the received video metadata.")
|
|
82
86
|
else:
|
|
83
87
|
raise RuntimeError(f"Could not download video with yt-dlp: {e}")
|
|
84
88
|
|
|
@@ -20,7 +20,7 @@ async def retrieve(
|
|
|
20
20
|
remove_urls: bool = False,
|
|
21
21
|
show_progress: bool = True,
|
|
22
22
|
actions: list[dict] = None,
|
|
23
|
-
methods: list[str] = None,
|
|
23
|
+
methods: list[str] | list[list[str]] = None,
|
|
24
24
|
format: str = "multimodal_sequence",
|
|
25
25
|
max_video_size: int | None = None,
|
|
26
26
|
) -> ScrapingResponse | list[ScrapingResponse]:
|
|
@@ -39,24 +39,24 @@ async def retrieve(
|
|
|
39
39
|
- "integrations" (API integrations for Twitter, Instagram, etc.)
|
|
40
40
|
- "firecrawl" (Firecrawl scraping service)
|
|
41
41
|
- "decodo" (Decodo Web Scraping API)
|
|
42
|
-
You can specify any subset in any order, e.g., ["decodo", "firecrawl"] or ["integrations"].
|
|
42
|
+
You can specify any subset in any order, e.g., ["decodo", "firecrawl"] or ["integrations"]. If provided
|
|
43
|
+
a list of strings, that order of methods will be applied to all submitted URLs. In contrast, if provided
|
|
44
|
+
a list of lists, each list will be applied to the corresponding URL in the batch.
|
|
43
45
|
:param format: The format of the output. Available formats:
|
|
44
46
|
- "multimodal_sequence" (MultimodalSequence containing parsed and downloaded media from the page)
|
|
45
47
|
- "html" (string containing the raw HTML code of the page, not compatible with 'integrations' method)
|
|
46
48
|
:param max_video_size: Maximum size of videos to download, in MB. If None, no limit is applied.
|
|
47
49
|
"""
|
|
48
50
|
if methods is None:
|
|
49
|
-
methods = METHODS
|
|
51
|
+
methods = METHODS.copy() # Use copy to avoid modifying the original list
|
|
50
52
|
else:
|
|
51
|
-
assert len(methods) >= 1
|
|
52
|
-
for method in methods:
|
|
53
|
-
assert method in METHODS
|
|
53
|
+
assert isinstance(methods, list) and len(methods) >= 1, "'methods' must be a non-empty list."
|
|
54
54
|
|
|
55
55
|
assert isinstance(urls, (str, list)), "'urls' must be a string or a list of strings."
|
|
56
56
|
|
|
57
57
|
# Ensure URLs are string or list
|
|
58
58
|
single_url = isinstance(urls, str)
|
|
59
|
-
urls_to_retrieve = [urls] if single_url else urls
|
|
59
|
+
urls_to_retrieve: list[str] = [urls] if single_url else urls
|
|
60
60
|
|
|
61
61
|
if len(urls_to_retrieve) == 0:
|
|
62
62
|
return []
|
|
@@ -64,15 +64,24 @@ async def retrieve(
|
|
|
64
64
|
if actions:
|
|
65
65
|
raise NotImplementedError("Actions are not supported yet.")
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
# Build per-URL method dict according to the provided 'methods'
|
|
68
|
+
if isinstance(methods[0], str):
|
|
69
|
+
# methods: list[str] → apply same order to all URLs
|
|
70
|
+
url_to_methods = {url: methods[:] for url in urls_to_retrieve}
|
|
71
|
+
elif isinstance(methods[0], list):
|
|
72
|
+
# methods: list[list[str]] → each inner list corresponds to the URL at the same index
|
|
73
|
+
url_to_methods = dict(zip(urls_to_retrieve, methods))
|
|
74
|
+
else:
|
|
75
|
+
raise AssertionError("'methods' must be either a list[str] or a list[list[str]]")
|
|
76
|
+
|
|
77
|
+
urls_unique = set(urls_to_retrieve)
|
|
70
78
|
|
|
79
|
+
async with aiohttp.ClientSession() as session:
|
|
71
80
|
# Retrieve URLs concurrently
|
|
72
|
-
tasks = [_retrieve_single(url, remove_urls, session,
|
|
81
|
+
tasks = [_retrieve_single(url, remove_urls, session, url_to_methods[url], actions,
|
|
73
82
|
format, max_video_size) for url in
|
|
74
83
|
urls_unique]
|
|
75
|
-
results = await run_with_semaphore(tasks, limit=20, show_progress=show_progress and len(
|
|
84
|
+
results = await run_with_semaphore(tasks, limit=20, show_progress=show_progress and len(urls_unique) > 1,
|
|
76
85
|
progress_description="Retrieving URLs...")
|
|
77
86
|
|
|
78
87
|
# Reconstruct output list
|
|
@@ -87,17 +96,24 @@ async def _retrieve_single(
|
|
|
87
96
|
url: str,
|
|
88
97
|
remove_urls: bool,
|
|
89
98
|
session: aiohttp.ClientSession,
|
|
90
|
-
methods: list[str],
|
|
99
|
+
methods: list[str] = None,
|
|
91
100
|
actions: list[dict] = None,
|
|
92
101
|
format: str = "multimodal_sequence",
|
|
93
102
|
max_video_size: int | None = None,
|
|
94
103
|
) -> ScrapingResponse:
|
|
95
104
|
logger.debug(f"Retrieving {url}")
|
|
96
105
|
|
|
106
|
+
if methods is None:
|
|
107
|
+
methods = METHODS.copy()
|
|
108
|
+
|
|
97
109
|
try:
|
|
98
110
|
# Ensure URL is a string
|
|
99
111
|
url = str(url)
|
|
100
112
|
|
|
113
|
+
# Validate methods
|
|
114
|
+
for method in methods:
|
|
115
|
+
assert method in METHODS, f"Unknown method '{method}'. Allowed: {METHODS}"
|
|
116
|
+
|
|
101
117
|
# Ensure compatibility with methods
|
|
102
118
|
if format == "html" and "integrations" in methods:
|
|
103
119
|
methods.remove("integrations")
|
|
@@ -2,10 +2,9 @@ from scrapemm import retrieve
|
|
|
2
2
|
import asyncio
|
|
3
3
|
|
|
4
4
|
if __name__ == "__main__":
|
|
5
|
-
url = "https://www.
|
|
5
|
+
url = "https://www.awesomescreenshot.com/image/43774028?key=0de2147873b73ed468d07aeb93512c51"
|
|
6
6
|
result = asyncio.run(retrieve(url))
|
|
7
7
|
if result.errors:
|
|
8
8
|
print(result.errors)
|
|
9
9
|
else:
|
|
10
10
|
print(result.content)
|
|
11
|
-
print(str(result.content))
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from ezmm import MultimodalSequence
|
|
3
|
+
from scrapemm.common import ScrapingResponse
|
|
4
|
+
|
|
5
|
+
from scrapemm import retrieve
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.asyncio
|
|
9
|
+
@pytest.mark.parametrize("url", [
|
|
10
|
+
"https://www.vishvasnews.com/viral/fact-check-upsc-has-not-reduced-the-maximum-age-limit-for-ias-and-ips-exams/",
|
|
11
|
+
"https://health.medicaldialogues.in/fact-check/brain-health-fact-check/fact-check-is-sprite-the-best-remedy-for-headaches-in-the-world-140368",
|
|
12
|
+
"https://www.washingtonpost.com/politics/2024/05/15/bidens-false-claim-that-inflation-was-9-percent-when-he-took-office/",
|
|
13
|
+
"https://assamese.factcrescendo.com/viral-claim-that-the-video-shows-the-incident-from-uttar-pradesh-and-the-youth-on-the-bike-and-the-youth-being-beaten-and-taken-away-by-the-police-are-the-same-youth-named-abdul-is-false/",
|
|
14
|
+
"https://factuel.afp.com/doc.afp.com.43ZN7NP",
|
|
15
|
+
"https://leadstories.com/365cb414b83e29d26fecae374d55c743a3eac4c7.png",
|
|
16
|
+
"https://leadstories.com/assets_c/2025/08/193f14f06dd6f15b89bf8050e553ad7fb1be6530-thumb-900xauto-3165872.png"
|
|
17
|
+
])
|
|
18
|
+
@pytest.mark.parametrize("method", ["firecrawl", "decodo"])
|
|
19
|
+
async def test_generic_retrieval(url, method):
|
|
20
|
+
result = await retrieve(url, methods=[method])
|
|
21
|
+
assert isinstance(result, ScrapingResponse)
|
|
22
|
+
print(result)
|
|
23
|
+
assert result
|
|
24
|
+
content = result.content
|
|
25
|
+
assert isinstance(content, MultimodalSequence)
|
|
26
|
+
assert content.has_images()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.asyncio
|
|
30
|
+
@pytest.mark.parametrize("url", [
|
|
31
|
+
"https://www.zeit.de/politik/deutschland/2025-07/spionage-iran-festnahme-anschlag-juden-berlin-daenemark",
|
|
32
|
+
"https://factnameh.com/fa/fact-checks/2025-04-16-araghchi-witkoff-fake-photo",
|
|
33
|
+
"https://www.thip.media/health-news-fact-check/fact-check-can-a-kalava-on-the-wrist-prevent-paralysis/74724/",
|
|
34
|
+
])
|
|
35
|
+
@pytest.mark.parametrize("method", ["firecrawl", "decodo"])
|
|
36
|
+
async def test_html_retrieval(url, method):
|
|
37
|
+
result = await retrieve(url, format="html", methods=[method])
|
|
38
|
+
assert isinstance(result, ScrapingResponse)
|
|
39
|
+
content = result.content
|
|
40
|
+
print(content)
|
|
41
|
+
assert content
|
|
42
|
+
assert isinstance(content, str)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.asyncio
|
|
46
|
+
@pytest.mark.parametrize("url, max_video_size, download_expected", [
|
|
47
|
+
("https://www.facebook.com/reel/1089214926521000", None, True),
|
|
48
|
+
("https://www.facebook.com/reel/1089214926521000", 128_000_000, True),
|
|
49
|
+
("https://www.facebook.com/reel/1089214926521000", 1_000_000, False),
|
|
50
|
+
("https://www.youtube.com/shorts/cE0zgN6pYOc", None, True),
|
|
51
|
+
("https://www.youtube.com/shorts/cE0zgN6pYOc", 4_000_000, True),
|
|
52
|
+
("https://www.youtube.com/shorts/cE0zgN6pYOc", 3_000_000, False),
|
|
53
|
+
])
|
|
54
|
+
async def test_max_video_size(url, max_video_size, download_expected):
|
|
55
|
+
result = await retrieve(url, max_video_size=max_video_size)
|
|
56
|
+
assert isinstance(result, ScrapingResponse)
|
|
57
|
+
content = result.content
|
|
58
|
+
assert isinstance(content, MultimodalSequence)
|
|
59
|
+
assert content.has_videos() == download_expected
|
|
60
|
+
if max_video_size and content.has_videos():
|
|
61
|
+
video = content.videos[0]
|
|
62
|
+
assert video.size <= max_video_size
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.asyncio
|
|
66
|
+
@pytest.mark.parametrize("urls, methods", [
|
|
67
|
+
([
|
|
68
|
+
"https://www.youtube.com/shorts/cE0zgN6pYOc"
|
|
69
|
+
], [
|
|
70
|
+
"integrations"
|
|
71
|
+
]),
|
|
72
|
+
([
|
|
73
|
+
"https://www.facebook.com/reel/1089214926521000",
|
|
74
|
+
"https://www.zeit.de/politik/deutschland/2025-07/spionage-iran-festnahme-anschlag-juden-berlin-daenemark",
|
|
75
|
+
], [
|
|
76
|
+
["integrations"],
|
|
77
|
+
["firecrawl"]
|
|
78
|
+
]),
|
|
79
|
+
([
|
|
80
|
+
"https://theconversation.com/dandelions-are-a-lifeline-for-bees-on-the-brink-we-should-learn-to-love-them-204504",
|
|
81
|
+
"https://yussus.wixsite.com/newsspoilers/post/leaked-durex-to-launch-reversible-condom-to-circumvent-single-use-plastic-law",
|
|
82
|
+
"https://www.bbc.co.uk/iplayer/episode/b09zg78h/question-time-2018-28062018#t=19m38s",
|
|
83
|
+
], [
|
|
84
|
+
"decodo",
|
|
85
|
+
]),
|
|
86
|
+
([
|
|
87
|
+
"https://factuel.afp.com/doc.afp.com.43ZN7NP",
|
|
88
|
+
"https://x.com/realDonaldTrump"
|
|
89
|
+
],
|
|
90
|
+
None
|
|
91
|
+
),
|
|
92
|
+
])
|
|
93
|
+
async def test_methods(urls: list[str], methods: list[str] | list[list[str]] | None):
|
|
94
|
+
results = await retrieve(urls, methods=methods)
|
|
95
|
+
assert results
|
|
96
|
+
if methods:
|
|
97
|
+
if isinstance(methods[0], str):
|
|
98
|
+
methods = [methods] * len(urls)
|
|
99
|
+
for result, method_list in zip(results, methods):
|
|
100
|
+
assert result.method in method_list
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from ezmm import MultimodalSequence
|
|
3
|
-
|
|
4
|
-
from scrapemm import retrieve
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@pytest.mark.asyncio
|
|
8
|
-
@pytest.mark.parametrize("url", [
|
|
9
|
-
"https://www.vishvasnews.com/viral/fact-check-upsc-has-not-reduced-the-maximum-age-limit-for-ias-and-ips-exams/",
|
|
10
|
-
"https://health.medicaldialogues.in/fact-check/brain-health-fact-check/fact-check-is-sprite-the-best-remedy-for-headaches-in-the-world-140368",
|
|
11
|
-
"https://www.washingtonpost.com/politics/2024/05/15/bidens-false-claim-that-inflation-was-9-percent-when-he-took-office/",
|
|
12
|
-
"https://assamese.factcrescendo.com/viral-claim-that-the-video-shows-the-incident-from-uttar-pradesh-and-the-youth-on-the-bike-and-the-youth-being-beaten-and-taken-away-by-the-police-are-the-same-youth-named-abdul-is-false/",
|
|
13
|
-
"https://factuel.afp.com/doc.afp.com.43ZN7NP",
|
|
14
|
-
"https://leadstories.com/365cb414b83e29d26fecae374d55c743a3eac4c7.png",
|
|
15
|
-
"https://leadstories.com/assets_c/2025/08/193f14f06dd6f15b89bf8050e553ad7fb1be6530-thumb-900xauto-3165872.png"
|
|
16
|
-
])
|
|
17
|
-
@pytest.mark.parametrize("method", ["firecrawl", "decodo"])
|
|
18
|
-
async def test_generic_retrieval(url, method):
|
|
19
|
-
result = await retrieve(url, methods=[method])
|
|
20
|
-
print(result)
|
|
21
|
-
assert result
|
|
22
|
-
content = result.content
|
|
23
|
-
assert isinstance(content, MultimodalSequence)
|
|
24
|
-
assert content.has_images()
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@pytest.mark.asyncio
|
|
28
|
-
@pytest.mark.parametrize("url", [
|
|
29
|
-
"https://www.zeit.de/politik/deutschland/2025-07/spionage-iran-festnahme-anschlag-juden-berlin-daenemark",
|
|
30
|
-
"https://factnameh.com/fa/fact-checks/2025-04-16-araghchi-witkoff-fake-photo",
|
|
31
|
-
"https://www.thip.media/health-news-fact-check/fact-check-can-a-kalava-on-the-wrist-prevent-paralysis/74724/",
|
|
32
|
-
])
|
|
33
|
-
@pytest.mark.parametrize("method", ["firecrawl", "decodo"])
|
|
34
|
-
async def test_html_retrieval(url, method):
|
|
35
|
-
result = await retrieve(url, format="html", methods=[method])
|
|
36
|
-
content = result.content
|
|
37
|
-
print(content)
|
|
38
|
-
assert content
|
|
39
|
-
assert isinstance(content, str)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@pytest.mark.asyncio
|
|
43
|
-
@pytest.mark.parametrize("url", [
|
|
44
|
-
"https://t.me/durov/404", # One image
|
|
45
|
-
"https://t.me/tglobaleye/16172", # Multiple images
|
|
46
|
-
"https://t.me/tglobaleye/16178", # Video and quote
|
|
47
|
-
"https://t.me/tglobaleye/6289", # GIF (treated as video)
|
|
48
|
-
"https://t.me/tglobaleye/16192", # Images and video
|
|
49
|
-
])
|
|
50
|
-
async def test_telegram(url):
|
|
51
|
-
result = await retrieve(url)
|
|
52
|
-
content = result.content
|
|
53
|
-
print(content)
|
|
54
|
-
assert content
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@pytest.mark.asyncio
|
|
58
|
-
@pytest.mark.parametrize("url", [
|
|
59
|
-
"https://www.tiktok.com/@realdonaldtrump/video/7433870905635409198",
|
|
60
|
-
"https://www.tiktok.com/@xxxx.xxxx5743/video/7521704371109793046"
|
|
61
|
-
])
|
|
62
|
-
async def test_tiktok(url):
|
|
63
|
-
result = await retrieve(url)
|
|
64
|
-
content = result.content
|
|
65
|
-
print(content)
|
|
66
|
-
assert content
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@pytest.mark.asyncio
|
|
70
|
-
@pytest.mark.parametrize("url", [
|
|
71
|
-
"https://x.com/PopBase/status/1938496291908030484",
|
|
72
|
-
"https://x.com/realDonaldTrump"
|
|
73
|
-
])
|
|
74
|
-
async def test_x(url):
|
|
75
|
-
result = await retrieve(url)
|
|
76
|
-
content = result.content
|
|
77
|
-
print(content)
|
|
78
|
-
assert content
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@pytest.mark.asyncio
|
|
82
|
-
@pytest.mark.parametrize("url", [
|
|
83
|
-
"https://www.instagram.com/p/CqJDbyOP839",
|
|
84
|
-
])
|
|
85
|
-
async def test_instagram_images(url):
|
|
86
|
-
result = await retrieve(url)
|
|
87
|
-
content = result.content
|
|
88
|
-
print(content)
|
|
89
|
-
assert content
|
|
90
|
-
assert content.has_images()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
@pytest.mark.asyncio
|
|
94
|
-
@pytest.mark.parametrize("url", [
|
|
95
|
-
"https://www.instagram.com/reel/DKqPQqpTDW4",
|
|
96
|
-
"https://www.instagram.com/reel/C75nh7Lvo8F",
|
|
97
|
-
"https://www.instagram.com/p/DMuOe6th94D", # yes, this is a video
|
|
98
|
-
])
|
|
99
|
-
async def test_instagram_videos(url):
|
|
100
|
-
result = await retrieve(url)
|
|
101
|
-
content = result.content
|
|
102
|
-
print(content)
|
|
103
|
-
assert content
|
|
104
|
-
assert content.has_videos()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@pytest.mark.asyncio
|
|
108
|
-
@pytest.mark.parametrize("url", [
|
|
109
|
-
"https://www.facebook.com/reel/1089214926521000",
|
|
110
|
-
"https://www.facebook.com/reel/3466446073497470", # restricted for misinformation
|
|
111
|
-
"https://www.facebook.com/61561558177010/videos/1445957793080961/",
|
|
112
|
-
"https://www.facebook.com/watch/?v=1445957793080961",
|
|
113
|
-
"https://www.facebook.com/groups/1973976962823632/posts/3992825270938781/", # restricted for misinformation, yt-dlp fails here
|
|
114
|
-
])
|
|
115
|
-
async def test_facebook_videos(url):
|
|
116
|
-
result = await retrieve(url)
|
|
117
|
-
content = result.content
|
|
118
|
-
print(content)
|
|
119
|
-
assert content
|
|
120
|
-
assert content.has_videos()
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
@pytest.mark.asyncio
|
|
124
|
-
@pytest.mark.parametrize("url", [
|
|
125
|
-
"https://www.facebook.com/photo/?fbid=1721085455188778&set=a.107961589834514&_rdc=1&_rdr",
|
|
126
|
-
])
|
|
127
|
-
async def test_facebook_images(url):
|
|
128
|
-
result = await retrieve(url)
|
|
129
|
-
content = result.content
|
|
130
|
-
print(content)
|
|
131
|
-
assert content
|
|
132
|
-
assert content.has_images()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
@pytest.mark.asyncio
|
|
136
|
-
@pytest.mark.parametrize("url", [
|
|
137
|
-
"https://www.youtube.com/watch?v=A4dVOznX6Kk",
|
|
138
|
-
"https://www.youtube.com/shorts/cE0zgN6pYOc",
|
|
139
|
-
])
|
|
140
|
-
async def test_youtube(url):
|
|
141
|
-
result = await retrieve(url)
|
|
142
|
-
content = result.content
|
|
143
|
-
print(content)
|
|
144
|
-
assert content
|
|
145
|
-
assert content.has_videos()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
@pytest.mark.asyncio
|
|
149
|
-
@pytest.mark.parametrize("url, max_video_size, download_expected", [
|
|
150
|
-
("https://www.facebook.com/reel/1089214926521000", None, True),
|
|
151
|
-
("https://www.facebook.com/reel/1089214926521000", 128_000_000, True),
|
|
152
|
-
("https://www.facebook.com/reel/1089214926521000", 1_000_000, False),
|
|
153
|
-
("https://www.youtube.com/shorts/cE0zgN6pYOc", None, True),
|
|
154
|
-
("https://www.youtube.com/shorts/cE0zgN6pYOc", 4_000_000, True),
|
|
155
|
-
("https://www.youtube.com/shorts/cE0zgN6pYOc", 3_000_000, False),
|
|
156
|
-
])
|
|
157
|
-
async def test_max_video_size(url, max_video_size, download_expected):
|
|
158
|
-
result = await retrieve(url, max_video_size=max_video_size)
|
|
159
|
-
content = result.content
|
|
160
|
-
assert content.has_videos() == download_expected
|
|
161
|
-
if max_video_size and content.has_videos():
|
|
162
|
-
video = content.videos[0]
|
|
163
|
-
assert video.size <= max_video_size
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|