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.
Files changed (37) hide show
  1. {scrapemm-0.4.1/scrapeMM.egg-info → scrapemm-0.4.2}/PKG-INFO +1 -1
  2. {scrapemm-0.4.1 → scrapemm-0.4.2}/pyproject.toml +7 -1
  3. {scrapemm-0.4.1 → scrapemm-0.4.2/scrapeMM.egg-info}/PKG-INFO +1 -1
  4. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/common/scraping_response.py +1 -1
  5. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/base.py +1 -1
  6. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/bluesky.py +6 -1
  7. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/fb.py +6 -3
  8. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/instagram.py +5 -3
  9. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/telegram.py +7 -2
  10. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/ytdlp.py +7 -3
  11. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/retrieval.py +29 -13
  12. {scrapemm-0.4.1 → scrapemm-0.4.2}/scripts/example.py +1 -2
  13. scrapemm-0.4.2/testing/test_retrieval.py +100 -0
  14. scrapemm-0.4.1/testing/test_retrieval.py +0 -163
  15. {scrapemm-0.4.1 → scrapemm-0.4.2}/.gitignore +0 -0
  16. {scrapemm-0.4.1 → scrapemm-0.4.2}/LICENSE +0 -0
  17. {scrapemm-0.4.1 → scrapemm-0.4.2}/README.md +0 -0
  18. {scrapemm-0.4.1 → scrapemm-0.4.2}/requirements.txt +0 -0
  19. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapeMM.egg-info/SOURCES.txt +0 -0
  20. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapeMM.egg-info/dependency_links.txt +0 -0
  21. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapeMM.egg-info/requires.txt +0 -0
  22. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapeMM.egg-info/top_level.txt +0 -0
  23. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/__init__.py +0 -0
  24. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/common/__init__.py +0 -0
  25. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/common/exceptions.py +0 -0
  26. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/__init__.py +0 -0
  27. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/decodo.py +0 -0
  28. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/firecrawl/__init__.py +0 -0
  29. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/firecrawl/firecrawl.py +0 -0
  30. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/firecrawl/no_bot_domains.txt +0 -0
  31. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/tiktok.py +0 -0
  32. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/x.py +0 -0
  33. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/integrations/youtube.py +0 -0
  34. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/secrets.py +0 -0
  35. {scrapemm-0.4.1 → scrapemm-0.4.2}/scrapemm/util.py +0 -0
  36. {scrapemm-0.4.1 → scrapemm-0.4.2}/setup.cfg +0 -0
  37. {scrapemm-0.4.1 → 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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrapeMM"
3
- version = "0.4.1"
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.1
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
@@ -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):
@@ -42,7 +42,12 @@ class Bluesky(RetrievalIntegration):
42
42
 
43
43
  return result
44
44
 
45
- async def _retrieve_post(self, url: str, session: aiohttp.ClientSession = None, max_video_size: int = None) -> Optional[MultimodalSequence]:
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", "www.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 or "facebook.com/reel" in url or bool(re.search(VIDEO_URL_REGEX, 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
- content = await self._get_video(url, **kwargs)
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
- logger.warning("Native Instagram photo download not yet supported. Use Decodo for that.")
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
- message = await self.client.get_messages(channel, ids=post_id)
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
- async with aiohttp.ClientSession() as session:
68
- # Remove duplicates
69
- urls_unique = set(urls_to_retrieve)
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, methods, actions,
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(urls_to_retrieve) > 1,
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.tiktok.com/@realdonaldtrump/video/7433870905635409198"
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