scrapeMM 0.4.2__tar.gz → 0.4.3__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 (36) hide show
  1. {scrapemm-0.4.2/scrapeMM.egg-info → scrapemm-0.4.3}/PKG-INFO +1 -1
  2. {scrapemm-0.4.2 → scrapemm-0.4.3}/pyproject.toml +1 -1
  3. {scrapemm-0.4.2 → scrapemm-0.4.3/scrapeMM.egg-info}/PKG-INFO +1 -1
  4. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/__init__.py +1 -1
  5. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/common/__init__.py +9 -0
  6. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/fb.py +10 -1
  7. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/x.py +15 -8
  8. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/ytdlp.py +3 -3
  9. {scrapemm-0.4.2 → scrapemm-0.4.3}/scripts/example.py +1 -1
  10. {scrapemm-0.4.2 → scrapemm-0.4.3}/.gitignore +0 -0
  11. {scrapemm-0.4.2 → scrapemm-0.4.3}/LICENSE +0 -0
  12. {scrapemm-0.4.2 → scrapemm-0.4.3}/README.md +0 -0
  13. {scrapemm-0.4.2 → scrapemm-0.4.3}/requirements.txt +0 -0
  14. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapeMM.egg-info/SOURCES.txt +0 -0
  15. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapeMM.egg-info/dependency_links.txt +0 -0
  16. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapeMM.egg-info/requires.txt +0 -0
  17. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapeMM.egg-info/top_level.txt +0 -0
  18. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/common/exceptions.py +0 -0
  19. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/common/scraping_response.py +0 -0
  20. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/__init__.py +0 -0
  21. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/base.py +0 -0
  22. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/bluesky.py +0 -0
  23. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/decodo.py +0 -0
  24. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/firecrawl/__init__.py +0 -0
  25. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/firecrawl/firecrawl.py +0 -0
  26. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/firecrawl/no_bot_domains.txt +0 -0
  27. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/instagram.py +0 -0
  28. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/telegram.py +0 -0
  29. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/tiktok.py +0 -0
  30. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/integrations/youtube.py +0 -0
  31. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/retrieval.py +0 -0
  32. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/secrets.py +0 -0
  33. {scrapemm-0.4.2 → scrapemm-0.4.3}/scrapemm/util.py +0 -0
  34. {scrapemm-0.4.2 → scrapemm-0.4.3}/setup.cfg +0 -0
  35. {scrapemm-0.4.2 → scrapemm-0.4.3}/testing/test_retrieval.py +0 -0
  36. {scrapemm-0.4.2 → scrapemm-0.4.3}/testing/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.4.2
3
+ Version: 0.4.3
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.2"
3
+ version = "0.4.3"
4
4
  description = "Flexible multimodal scraper for social media and the open web."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.4.2
3
+ Version: 0.4.3
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
@@ -2,7 +2,7 @@ import logging
2
2
  import sys
3
3
 
4
4
  import scrapemm.common
5
- from .common import APP_NAME
5
+ from .common import APP_NAME, set_wait_on_rate_limit, RateLimitError, ContentNotFoundError
6
6
  from .integrations import Telegram, X
7
7
  from .retrieval import retrieve
8
8
  from .secrets import configure_secrets
@@ -5,6 +5,7 @@ import yaml
5
5
  from platformdirs import user_config_dir
6
6
 
7
7
  from .scraping_response import ScrapingResponse
8
+ from .exceptions import RateLimitError, ContentNotFoundError
8
9
 
9
10
  APP_NAME = "scrapeMM"
10
11
 
@@ -13,6 +14,7 @@ CONFIG_DIR = Path(user_config_dir(APP_NAME))
13
14
  os.makedirs(CONFIG_DIR, exist_ok=True)
14
15
  CONFIG_PATH = CONFIG_DIR / "config.yaml"
15
16
 
17
+ WAIT_ON_RATE_LIMIT = False
16
18
 
17
19
  def load_config() -> dict:
18
20
  if os.path.exists(CONFIG_PATH):
@@ -31,5 +33,12 @@ def get_config_var(name: str, default=None) -> str:
31
33
  return _config.get(name, default)
32
34
 
33
35
 
36
+ def set_wait_on_rate_limit(wait: bool):
37
+ """Set whether to wait on rate limits (particularly relevant for X API).
38
+ Will result in a RateLimitError otherwise."""
39
+ global WAIT_ON_RATE_LIMIT
40
+ WAIT_ON_RATE_LIMIT = wait
41
+
42
+
34
43
  # Load config
35
44
  _config = load_config()
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  import re
3
- from urllib.parse import urlparse
3
+ from urllib.parse import urlparse, unquote
4
4
 
5
5
  from ezmm import MultimodalSequence
6
6
 
@@ -24,6 +24,8 @@ 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
+ url = self._normalize_url(url)
28
+
27
29
  # Determine if this is a video or photo URL, act accordingly
28
30
  if self._is_video_url(url):
29
31
  return await self._get_video(url, **kwargs)
@@ -50,6 +52,13 @@ class Facebook(RetrievalIntegration):
50
52
  """Retrieves content from a Facebook user profile URL."""
51
53
  raise NotImplementedError("No available method to retrieve Facebook profiles.")
52
54
 
55
+ def _normalize_url(self, url: str) -> str:
56
+ """If the URL is a login Facebook URL, i.e., of the form https://www.facebook.com/login/?next=...
57
+ extracts the actual post's URL."""
58
+ if url.startswith("https://www.facebook.com/login/?next="):
59
+ url = unquote(url.split("?next=")[1])
60
+ return url
61
+
53
62
  def _is_video_url(self, url: str) -> bool:
54
63
  """Checks if the URL is a Facebook video URL."""
55
64
  # video URLS are in the format: https://www.facebook.com/watch?v=VIDEO_ID or fb.watch/...
@@ -1,14 +1,17 @@
1
1
  import asyncio
2
2
  import logging
3
3
  import re
4
+ import traceback
4
5
  from typing import Optional
5
6
  from urllib.parse import urlparse
6
7
 
7
8
  import aiohttp
8
9
  from ezmm import MultimodalSequence, download_video, download_image
9
- from tweepy import Tweet, User
10
+ from tweepy import Tweet, User, BadRequest, TooManyRequests
10
11
  from tweepy.asynchronous import AsyncClient
11
12
 
13
+ import scrapemm.common
14
+ from scrapemm.common.exceptions import RateLimitError
12
15
  from scrapemm.secrets import get_secret
13
16
  from scrapemm.integrations.base import RetrievalIntegration
14
17
 
@@ -40,7 +43,7 @@ class X(RetrievalIntegration):
40
43
  async def _connect(self):
41
44
  bearer_token = get_secret("x_bearer_token")
42
45
  if bearer_token:
43
- self.client = AsyncClient(bearer_token=bearer_token, wait_on_rate_limit=True)
46
+ self.client = AsyncClient(bearer_token=bearer_token, wait_on_rate_limit=scrapemm.common.WAIT_ON_RATE_LIMIT)
44
47
  self.connected = True
45
48
  logger.info("✅ Successfully connected to X.")
46
49
  else:
@@ -51,12 +54,16 @@ class X(RetrievalIntegration):
51
54
  session = kwargs.get("session")
52
55
  max_video_size = kwargs.get("max_video_size")
53
56
  tweet_id = extract_tweet_id_from_url(url)
54
- if tweet_id:
55
- return await self._get_tweet(tweet_id, session, max_video_size)
56
- else:
57
- username = extract_username_from_url(url)
58
- if username:
59
- return await self._get_user(username, session)
57
+ try:
58
+ if tweet_id:
59
+ return await self._get_tweet(tweet_id, session, max_video_size)
60
+ else:
61
+ username = extract_username_from_url(url)
62
+ if username:
63
+ return await self._get_user(username, session)
64
+ except TooManyRequests:
65
+ raise RateLimitError("X API rate limit reached.")
66
+
60
67
 
61
68
  async def _get_tweet(self, tweet_id: int, session: aiohttp.ClientSession, max_video_size: int = None) -> Optional[MultimodalSequence]:
62
69
  """Returns a MultimodalSequence containing the tweet's text and media
@@ -2,7 +2,6 @@ import asyncio
2
2
  import logging
3
3
  import sys
4
4
  import tempfile
5
- import traceback
6
5
  from datetime import datetime
7
6
  from typing import Any, Optional
8
7
 
@@ -10,7 +9,7 @@ import aiohttp
10
9
  from ezmm import MultimodalSequence, download_image, Video, Image
11
10
  from yt_dlp import YoutubeDL
12
11
 
13
- from scrapemm.common.exceptions import IPBannedError, ContentNotFoundError
12
+ from scrapemm.common.exceptions import ContentNotFoundError
14
13
 
15
14
  logger = logging.getLogger("scrapeMM")
16
15
 
@@ -79,7 +78,8 @@ async def download_video_with_ytdlp(
79
78
  if "The following content is not available on this app" in str(e):
80
79
  logger.warning(f"You should update yt-dlp to re-enable YouTube downloads.")
81
80
  raise e
82
- elif "Video unavailable" in str(e):
81
+ elif ("Video unavailable" in str(e)
82
+ or "HTTP Error 404: Not Found" in str(e)):
83
83
  raise ContentNotFoundError(f"Video is unavailable.")
84
84
  elif "Cannot parse data; please report this issue" in str(e):
85
85
  raise RuntimeError(f"yt-dlp is unable to parse the received video metadata.")
@@ -2,7 +2,7 @@ from scrapemm import retrieve
2
2
  import asyncio
3
3
 
4
4
  if __name__ == "__main__":
5
- url = "https://www.awesomescreenshot.com/image/43774028?key=0de2147873b73ed468d07aeb93512c51"
5
+ url = "https://x.com/Shayan86/status/1891673533601780022"
6
6
  result = asyncio.run(retrieve(url))
7
7
  if result.errors:
8
8
  print(result.errors)
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