warp-beacon 2.0.10__py3-none-any.whl → 2.1.1__py3-none-any.whl

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.
@@ -1,14 +1,15 @@
1
1
  TG_TOKEN=""
2
+ TG_BOT_NAME=""
3
+ TG_BOT_ADMIN_USERNAME=""
2
4
  TG_API_ID=""
3
5
  TG_API_HASH=""
4
6
  TG_BOT_NAME=""
5
7
  TG_BOT_ADMIN_USERNAME=""
6
- INSTAGRAM_LOGIN=""
7
- INSTAGRAM_PASSWORD=""
8
- INSTAGRAM_VERIFICATION_CODE=""
8
+ IG_MAX_RETRIES=10
9
9
  MONGODB_HOST="mongodb"
10
10
  MONGODB_PORT="27017"
11
11
  MONGODB_USER="root"
12
12
  MONGODB_PASSWORD="changeme"
13
- VIDEO_STORAGE_DIR="/var/warp_beacon/videos"
14
- ENABLE_DONATES=true
13
+ ENABLE_DONATES=true
14
+ SERVICE_ACCOUNTS_FILE=/var/warp_beacon/accounts.json
15
+ FORCE_IPV6=true
@@ -0,0 +1,61 @@
1
+ {
2
+ "instagram":
3
+ [
4
+ {
5
+ "login": "ig_account0",
6
+ "password": "ig_password",
7
+ "imap_server": "imap.gmail.com",
8
+ "imap_login": "user@gmail.com",
9
+ "imap_password": "",
10
+ "auth_details":
11
+ {
12
+ "delay_range": [1, 3],
13
+ "country_code": 7,
14
+ "locale": "en_US",
15
+ "timezone_offset": 10800,
16
+ "user_agent": "Barcelona 291.0.0.31.111 Android (33/13; 600dpi; 1440x3044; samsung; SM-G998B; p3s; exynos2100; en_US; 493450264)",
17
+ "device":
18
+ {
19
+ "app_version": "291.0.0.31.111",
20
+ "android_version": 33,
21
+ "android_release": "13.0.0",
22
+ "dpi": "600dpi",
23
+ "resolution": "1440x3044",
24
+ "manufacturer": "Samsung",
25
+ "device": "p3s",
26
+ "model": "SM-G998B",
27
+ "cpu": "exynos2100",
28
+ "version_code": "493450264"
29
+ }
30
+ }
31
+ },
32
+ {
33
+ "login": "ig_account1",
34
+ "password": "passwd",
35
+ "imap_server": "imap.gmail.com",
36
+ "imap_login": "mail_login1",
37
+ "imap_password": "imap_password1",
38
+ "auth_details":
39
+ {
40
+ "delay_range": [1, 3],
41
+ "country_code": 7,
42
+ "locale": "en_US",
43
+ "timezone_offset": 10800,
44
+ "user_agent": "Barcelona 291.0.0.31.111 Android (33/13; 600dpi; 1440x3044; samsung; SM-G998B; p3s; exynos2100; en_US; 493450264)",
45
+ "device":
46
+ {
47
+ "app_version": "291.0.0.31.111",
48
+ "android_version": 33,
49
+ "android_release": "13.0.0",
50
+ "dpi": "600dpi",
51
+ "resolution": "1440x3044",
52
+ "manufacturer": "Samsung",
53
+ "device": "p3s",
54
+ "model": "SM-G998B",
55
+ "cpu": "exynos2100",
56
+ "version_code": "493450264"
57
+ }
58
+ }
59
+ }
60
+ ]
61
+ }
@@ -1,2 +1,2 @@
1
- __version__ = "2.0.10"
1
+ __version__ = "2.1.1"
2
2
 
@@ -31,6 +31,9 @@ class JobSettings(TypedDict):
31
31
  canonical_name: str
32
32
  is_message_to_admin: bool
33
33
  message_text: str
34
+ source_username: str
35
+ unvailable_error_count: int
36
+ geoblock_error_count: int
34
37
 
35
38
  class AbstractJob(ABC):
36
39
  job_id: uuid.UUID = None
@@ -56,6 +59,9 @@ class AbstractJob(ABC):
56
59
  canonical_name: str = ""
57
60
  is_message_to_admin: bool = False
58
61
  message_text: str = ""
62
+ source_usename: str = ""
63
+ unvailable_error_count: int = 0
64
+ geoblock_error_count: int = 0
59
65
 
60
66
  def __init__(self, **kwargs: Unpack[JobSettings]) -> None:
61
67
  if kwargs:
@@ -1,12 +1,10 @@
1
1
  import os
2
- import time
3
- import asyncio
4
2
 
5
3
  from typing import Optional
6
4
  import multiprocessing
7
5
  from queue import Empty
8
6
 
9
- from warp_beacon.scraper.exceptions import NotFound, UnknownError, TimeOut, Unavailable, FileTooBig, YotubeLiveError, YotubeAgeRestrictedError
7
+ from warp_beacon.scraper.exceptions import NotFound, UnknownError, TimeOut, Unavailable, FileTooBig, YotubeLiveError, YotubeAgeRestrictedError, IGRateLimitAccured
10
8
  from warp_beacon.mediainfo.video import VideoInfo
11
9
  from warp_beacon.mediainfo.audio import AudioInfo
12
10
  from warp_beacon.mediainfo.silencer import Silencer
@@ -16,9 +14,12 @@ from warp_beacon.jobs import Origin
16
14
  from warp_beacon.jobs.download_job import DownloadJob
17
15
  from warp_beacon.jobs.upload_job import UploadJob
18
16
  from warp_beacon.jobs.types import JobType
17
+ from warp_beacon.scraper.account_selector import AccountSelector
19
18
 
20
19
  import logging
21
20
 
21
+ ACC_FILE = os.environ.get("SERVICE_ACCOUNTS_FILE", default="/var/warp_beacon/instagram_accounts.json")
22
+
22
23
  class AsyncDownloader(object):
23
24
  __JOE_BIDEN_WAKEUP = None
24
25
  workers = []
@@ -27,11 +28,13 @@ class AsyncDownloader(object):
27
28
  uploader = None
28
29
  workers_count = 0
29
30
  auth_event = multiprocessing.Event()
31
+ acc_selector = None
30
32
 
31
33
  def __init__(self, uploader: AsyncUploader, workers_count: int) -> None:
32
34
  self.allow_loop = multiprocessing.Value('i', 1)
33
35
  self.uploader = uploader
34
36
  self.workers_count = workers_count
37
+ self.acc_selector = AccountSelector(ACC_FILE)
35
38
 
36
39
  def __del__(self) -> None:
37
40
  self.stop_all()
@@ -77,18 +80,19 @@ class AsyncDownloader(object):
77
80
  if job.job_origin is not Origin.UNKNOWN:
78
81
  if not job.in_process:
79
82
  actor = None
83
+ self.acc_selector.set_module(job.job_origin)
80
84
  if job.job_origin is Origin.INSTAGRAM:
81
- from warp_beacon.scraper.instagram import InstagramScraper
82
- actor = InstagramScraper()
85
+ from warp_beacon.scraper.instagram.instagram import InstagramScraper
86
+ actor = InstagramScraper(self.acc_selector.get_current())
83
87
  elif job.job_origin is Origin.YT_SHORTS:
84
88
  from warp_beacon.scraper.youtube.shorts import YoutubeShortsScraper
85
- actor = YoutubeShortsScraper()
89
+ actor = YoutubeShortsScraper(self.acc_selector.get_current())
86
90
  elif job.job_origin is Origin.YT_MUSIC:
87
91
  from warp_beacon.scraper.youtube.music import YoutubeMusicScraper
88
- actor = YoutubeMusicScraper()
92
+ actor = YoutubeMusicScraper(self.acc_selector.get_current())
89
93
  elif job.job_origin is Origin.YOUTUBE:
90
94
  from warp_beacon.scraper.youtube.youtube import YoutubeScraper
91
- actor = YoutubeScraper()
95
+ actor = YoutubeScraper(self.acc_selector.get_current())
92
96
  #self.auth_event = multiprocessing.Event()
93
97
  actor.send_message_to_admin_func = self.send_message_to_admin
94
98
  actor.auth_event = self.auth_event
@@ -97,14 +101,28 @@ class AsyncDownloader(object):
97
101
  logging.info("Downloading URL '%s'", job.url)
98
102
  items = actor.download(job.url)
99
103
  break
100
- except (NotFound, Unavailable) as e:
101
- logging.warning("Not found or unavailable error occurred!")
104
+ except NotFound as e:
105
+ logging.warning("Not found error occurred!")
102
106
  logging.exception(e)
103
107
  self.uploader.queue_task(job.to_upload_job(
104
108
  job_failed=True,
105
109
  job_failed_msg="Unable to access to media under this URL. Seems like the media is private.")
106
110
  )
107
111
  break
112
+ except Unavailable as e:
113
+ logging.warning("Not found or unavailable error occurred!")
114
+ logging.exception(e)
115
+ if job.unvailable_error_count > self.acc_selector.count_service_accounts(job.job_origin.value):
116
+ self.uploader.queue_task(job.to_upload_job(
117
+ job_failed=True,
118
+ job_failed_msg="Video is unvailable for all your service accounts.")
119
+ )
120
+ break
121
+ job.unvailable_error_count += 1
122
+ logging.info("Trying to switch account")
123
+ self.acc_selector.next()
124
+ self.job_queue.put(job)
125
+ break
108
126
  except TimeOut as e:
109
127
  logging.warning("Timeout error occurred!")
110
128
  logging.exception(e)
@@ -121,6 +139,14 @@ class AsyncDownloader(object):
121
139
  job_failed_msg="Unfortunately this file has exceeded the Telegram limits. A file cannot be larger than 2 gigabytes.")
122
140
  )
123
141
  break
142
+ except IGRateLimitAccured as e:
143
+ logging.warning("IG ratelimit accured :(")
144
+ logging.exception(e)
145
+ logging.warning("Switching Instagram account!")
146
+ self.acc_selector.bump_acc_fail("rate_limits")
147
+ self.acc_selector.next()
148
+ cur_acc = self.acc_selector.get_current()
149
+ logging.info("Current Instagram account: index: '%d', login: '%s'", cur_acc[0], cur_acc[1]["login"])
124
150
  except YotubeLiveError as e:
125
151
  logging.warning("Youtube Live videos are not supported. Skipping.")
126
152
  logging.exception(e)
@@ -146,10 +172,16 @@ class AsyncDownloader(object):
146
172
  else:
147
173
  exception_msg = str(e)
148
174
  if "geoblock_required" in exception_msg:
149
- self.uploader.queue_task(job.to_upload_job(
150
- job_failed=True,
151
- job_failed_msg="This content does not accessible for bot account. Seems like author blocked certain region.")
152
- )
175
+ if job.geoblock_error_count > self.acc_selector.count_service_accounts(job.job_origin.value):
176
+ self.uploader.queue_task(job.to_upload_job(
177
+ job_failed=True,
178
+ job_failed_msg="This content does not accessible for all yout bot accounts. Seems like author blocked certain regions.")
179
+ )
180
+ break
181
+ job.geoblock_error_count += 1
182
+ logging.info("Trying to switch account")
183
+ self.acc_selector.next()
184
+ self.job_queue.put(job)
153
185
  break
154
186
  self.uploader.queue_task(job.to_upload_job(
155
187
  job_failed=True,
@@ -1,6 +1,9 @@
1
1
  import os
2
2
  import pathlib
3
3
 
4
+ import socket
5
+ import requests.packages.urllib3.util.connection as urllib3_cn
6
+
4
7
  from abc import ABC, abstractmethod
5
8
  from typing import Callable, Union
6
9
 
@@ -10,14 +13,21 @@ from pillow_heif import register_heif_opener
10
13
  import logging
11
14
 
12
15
  class ScraperAbstract(ABC):
16
+ original_gai_family = None
13
17
  send_message_to_admin_func = None
14
18
  auth_event = None
19
+ account = None
20
+ account_index = 0
15
21
 
16
- def __init__(self) -> None:
17
- pass
22
+ def __init__(self, account: tuple) -> None:
23
+ self.account_index = account[0]
24
+ self.account = account[1]
25
+ if os.environ.get("FORCE_IPV6", default="false") == "true":
26
+ self.force_ipv6()
18
27
 
19
28
  def __del__(self) -> None:
20
- pass
29
+ if os.environ.get("FORCE_IPV6", default="false") == "true":
30
+ self.restore_gai()
21
31
 
22
32
  @abstractmethod
23
33
  def download(self, url: str) -> bool:
@@ -63,3 +73,21 @@ class ScraperAbstract(ABC):
63
73
  logging.exception(e)
64
74
 
65
75
  return ''
76
+
77
+ def force_ipv6(self) -> None:
78
+ def allowed_gai_family():
79
+ """
80
+ https://github.com/shazow/urllib3/blob/master/urllib3/util/connection.py
81
+ """
82
+ family = socket.AF_INET
83
+ if urllib3_cn.HAS_IPV6:
84
+ family = socket.AF_INET6 # force ipv6 only if it is available
85
+ return family
86
+ if self.original_gai_family is None:
87
+ self.original_gai_family = urllib3_cn.allowed_gai_family
88
+ logging.info("Forcing IPv6 ...")
89
+ urllib3_cn.allowed_gai_family = allowed_gai_family
90
+
91
+ def restore_gai(self) -> None:
92
+ logging.info("Restoring normal IP stack ...")
93
+ urllib3_cn.allowed_gai_family = self.original_gai_family
@@ -0,0 +1,81 @@
1
+ import os
2
+ import json
3
+ import re
4
+
5
+ from itertools import cycle
6
+
7
+ from warp_beacon.jobs import Origin
8
+
9
+ import logging
10
+
11
+ class AccountSelector(object):
12
+ accounts = []
13
+ acc_pool = None
14
+ current = None
15
+ current_module_name = None
16
+ index = 0
17
+ accounts_meta_data = {}
18
+ session_dir = "/var/warp_beacon"
19
+
20
+ def __init__(self, acc_file_path: str) -> None:
21
+ if os.path.exists(acc_file_path):
22
+ with open(acc_file_path, 'r', encoding="utf-8") as f:
23
+ self.accounts = json.loads(f.read())
24
+ if self.accounts:
25
+ self.__init_meta_data()
26
+ self.load_yt_sessions()
27
+ else:
28
+ raise ValueError("Accounts file not found")
29
+
30
+ def __del__(self) -> None:
31
+ pass
32
+
33
+ #def enrich_service_data(self) -> None:
34
+ # for k, v in self.accounts.items():
35
+
36
+ def load_yt_sessions(self) -> None:
37
+ self.accounts["youtube"] = []
38
+ for f in os.listdir(self.session_dir):
39
+ if "yt_session" in f and ".json" in f:
40
+ match = re.search('\d+', f)
41
+ index = 0
42
+ if match:
43
+ index = int(match.group(0))
44
+ self.accounts["youtube"].insert(index, {"session_file": "%s/%s" % (self.session_dir, f), "index": index})
45
+ if not self.accounts["youtube"]:
46
+ self.accounts["youtube"].append({"session_file": "%s/yt_session_0.json" % self.session_dir, "index": 0})
47
+
48
+ def __init_meta_data(self) -> None:
49
+ for module_name, lst in self.accounts.items():
50
+ if module_name not in self.accounts_meta_data:
51
+ self.accounts_meta_data[module_name] = []
52
+ for index, _ in enumerate(lst):
53
+ self.accounts_meta_data[module_name].insert(index, {"auth_fails": 0, "rate_limits": 0})
54
+
55
+ def set_module(self, module_origin: Origin) -> None:
56
+ module_name = 'youtube' if next((s for s in ("yt", "youtube", "youtu_be") if s in module_origin.value), None) else 'instagram'
57
+ self.current_module_name = module_name
58
+ self.acc_pool = cycle(self.accounts[module_name])
59
+ self.current = next(self.acc_pool)
60
+ self.index = self.accounts[module_name].index(self.current)
61
+
62
+ def next(self) -> dict:
63
+ self.current = next(self.acc_pool)
64
+ self.index = self.accounts[self.current_module_name].index(self.current)
65
+ return self.current
66
+
67
+ def bump_acc_fail(self, key: str, amount: int = 1) -> int:
68
+ self.accounts_meta_data[self.index][key] += amount
69
+ return self.accounts_meta_data[self.index][key]
70
+
71
+ def how_much(self, key: str) -> int:
72
+ return self.accounts_meta_data[self.current_module_name][self.index][key]
73
+
74
+ def get_current(self) -> tuple:
75
+ return (self.index, self.current)
76
+
77
+ def get_meta_data(self) -> dict:
78
+ return self.accounts_meta_data[self.current_module_name][self.index]
79
+
80
+ def count_service_accounts(self, mod_name: str) -> int:
81
+ return len(self.accounts_meta_data[mod_name])
@@ -34,6 +34,9 @@ class YotubeLiveError(ScraperError):
34
34
  class YotubeAgeRestrictedError(ScraperError):
35
35
  pass
36
36
 
37
+ class IGRateLimitAccured(ScraperError):
38
+ pass
39
+
37
40
  class UnknownError(ScraperError):
38
41
  pass
39
42
 
File without changes
@@ -1,64 +1,103 @@
1
1
  import os
2
2
  import time
3
-
4
3
  import socket
5
4
  import ssl
6
-
5
+ import re
7
6
  from typing import Callable, Optional, Union
8
7
 
9
- from pathlib import Path
8
+ import random
9
+ import email
10
+ import imaplib
10
11
  import json
11
-
12
12
  import requests
13
13
  import urllib3
14
14
  from urllib.parse import urljoin, urlparse
15
15
 
16
16
  from instagrapi.mixins.story import Story
17
- from instagrapi.types import Media
17
+ #from instagrapi.types import Media
18
18
  from instagrapi import Client
19
+ from instagrapi.mixins.challenge import ChallengeChoice
19
20
  from instagrapi.exceptions import LoginRequired, PleaseWaitFewMinutes, MediaNotFound, ClientNotFoundError, UserNotFound, UnknownError as IGUnknownError
20
21
 
21
- from warp_beacon.scraper.exceptions import NotFound, UnknownError, TimeOut, extract_exception_message
22
+ from warp_beacon.scraper.exceptions import NotFound, UnknownError, TimeOut, IGRateLimitAccured, extract_exception_message
22
23
  from warp_beacon.scraper.abstract import ScraperAbstract
23
24
  from warp_beacon.jobs.types import JobType
24
25
  from warp_beacon.telegram.utils import Utils
25
26
 
26
27
  import logging
27
28
 
28
- INST_SESSION_FILE = "/var/warp_beacon/inst_session.json"
29
+ INST_SESSION_FILE_TPL = "/var/warp_beacon/inst_session_account_%d.json"
29
30
 
30
31
  class InstagramScraper(ScraperAbstract):
31
32
  cl = None
33
+ inst_session_file = ""
32
34
 
33
- def __init__(self) -> None:
34
- super().__init__()
35
+ def __init__(self, account: tuple) -> None:
36
+ super().__init__(account)
37
+ #
38
+ self.inst_session_file = INST_SESSION_FILE_TPL % self.account_index
35
39
  self.cl = Client()
40
+ self.setup_device()
41
+ self.cl.challenge_code_handler = self.challenge_code_handler
42
+ self.cl.change_password_handler = self.change_password_handler
43
+
44
+ def setup_device(self) -> None:
45
+ details = self.account.get("auth_details", {})
46
+ self.cl.delay_range = details.get("delay_range", [1, 3])
47
+ self.cl.set_country_code(details.get("country_code", 1))
48
+ self.cl.set_locale(details.get("locale", "en_US"))
49
+ self.cl.set_timezone_offset(details.get("timezone_offset", 10800))
50
+ self.cl.set_user_agent(details.get("user_agent", "Barcelona 291.0.0.31.111 Android (33/13; 600dpi; 1440x3044; samsung; SM-G998B; p3s; exynos2100; en_US; 493450264)"))
51
+ device = details.get("device", {})
52
+ self.cl.set_device({
53
+ "app_version": device.get("app_version", "291.0.0.31.111"),
54
+ "android_version": device.get("android_version", 33),
55
+ "android_release": device.get("android_release", "13.0.0"),
56
+ "dpi": device.get("dpi", "600dpi"),
57
+ "resolution": device.get("resolution", "1440x3044"),
58
+ "manufacturer": device.get("manufacturer", "Samsung"),
59
+ "device": device.get("device", "p3s"),
60
+ "model": device.get("model", "SM-G998B"),
61
+ "cpu": device.get("cpu", "exynos2100"),
62
+ "version_code": device.get("version_code", "493450264")
63
+ })
36
64
 
37
65
  def safe_write_session(self) -> None:
38
- tmp_fname = "%s~" % INST_SESSION_FILE
39
- with open(tmp_fname, 'w+') as f:
66
+ tmp_fname = "%s~" % self.inst_session_file
67
+ with open(tmp_fname, 'w+', encoding="utf-8") as f:
40
68
  f.write(json.dumps(self.cl.get_settings()))
41
- if os.path.isfile(INST_SESSION_FILE):
42
- os.unlink(INST_SESSION_FILE)
43
- os.rename(tmp_fname, INST_SESSION_FILE)
69
+ if os.path.exists(self.inst_session_file):
70
+ os.unlink(self.inst_session_file)
71
+ os.rename(tmp_fname, self.inst_session_file)
44
72
 
45
73
  def load_session(self) -> None:
46
- if os.path.exists(INST_SESSION_FILE):
47
- self.cl.load_settings(INST_SESSION_FILE)
74
+ if os.path.exists(self.inst_session_file):
75
+ self.cl.load_settings(self.inst_session_file)
48
76
  else:
49
77
  self.login()
50
78
 
51
79
  def login(self) -> None:
52
- self.cl = Client()
53
- username = os.environ.get("INSTAGRAM_LOGIN", default=None)
54
- password = os.environ.get("INSTAGRAM_PASSWORD", default=None)
55
- verification_code = os.environ.get("INSTAGRAM_VERIFICATION_CODE", default="")
56
- if username is not None and password is not None:
57
- self.cl.login(username=username, password=password, verification_code=verification_code)
80
+ username = self.account["login"]
81
+ password = self.account["password"]
82
+ if username and password:
83
+ self.cl.login(username=username, password=password, verification_code="")
58
84
  self.safe_write_session()
59
85
 
60
86
  def scrap(self, url: str) -> tuple[str]:
61
87
  self.load_session()
88
+ try:
89
+ self.cl.get_timeline_feed()
90
+ except LoginRequired as e:
91
+ logging.error("Exception occurred while cheking IG session!")
92
+ logging.exception(e)
93
+ old_session = self.cl.get_settings()
94
+ self.cl.set_settings({})
95
+ self.setup_device()
96
+ self.cl.set_uuids(old_session["uuids"])
97
+ if os.path.exists(self.inst_session_file):
98
+ os.unlink(self.inst_session_file)
99
+ time.sleep(5)
100
+ return self.scrap(url)
62
101
  def _scrap() -> tuple[str]:
63
102
  if "stories" in url:
64
103
  # remove URL options
@@ -102,6 +141,17 @@ class InstagramScraper(ScraperAbstract):
102
141
  try:
103
142
  ret_val = func(*args, **kwargs)
104
143
  break
144
+ except LoginRequired as e:
145
+ logging.error("LoginRequired occurred in download handler!")
146
+ logging.exception(e)
147
+ old_session = self.cl.get_settings()
148
+ self.cl.set_settings({})
149
+ self.setup_device()
150
+ self.cl.set_uuids(old_session["uuids"])
151
+ if os.path.exists(self.inst_session_file):
152
+ os.unlink(self.inst_session_file)
153
+ time.sleep(5)
154
+ self.load_session()
105
155
  except (socket.timeout,
106
156
  ssl.SSLError,
107
157
  requests.exceptions.ConnectionError,
@@ -173,7 +223,7 @@ class InstagramScraper(ScraperAbstract):
173
223
  for media_chunk in Utils.chunker(media_info.resources, 10):
174
224
  chunk = []
175
225
  for media in media_chunk:
176
- _media_info = self.cl.media_info(media.pk)
226
+ _media_info = self._download_hndlr(self.cl.media_info, media.pk)
177
227
  if media.media_type == 1: # photo
178
228
  chunk.append(self.download_photo(url=_media_info.thumbnail_url))
179
229
  elif media.media_type == 2: # video
@@ -184,6 +234,10 @@ class InstagramScraper(ScraperAbstract):
184
234
 
185
235
  def download(self, url: str) -> Optional[list[dict]]:
186
236
  res = []
237
+ wait_timeout = int(os.environ.get("IG_WAIT_TIMEOUT", default=60))
238
+ timeout_increment = int(os.environ.get("IG_TIMEOUT_INCREMENT", default=30))
239
+ ratelimit_threshold = int(os.environ.get("IG_RATELIMIT_TRESHOLD", default=5))
240
+ please_wait_few_minutes_count = 1
187
241
  while True:
188
242
  try:
189
243
  scrap_type, media_id = self.scrap(url)
@@ -205,10 +259,14 @@ class InstagramScraper(ScraperAbstract):
205
259
  res.append(self.download_stories(self.scrap_stories(media_id)))
206
260
  break
207
261
  except PleaseWaitFewMinutes as e:
262
+ please_wait_few_minutes_count += 1
208
263
  logging.warning("Please wait a few minutes error. Trying to relogin ...")
209
264
  logging.exception(e)
210
- wait_timeout = int(os.environ.get("IG_WAIT_TIMEOUT", default=5))
211
- logging.info("Waiting %d seconds according configuration option `IG_WAIT_TIMEOUT`", wait_timeout)
265
+ if please_wait_few_minutes_count >= ratelimit_threshold:
266
+ logging.warning("IG ratelimit accured")
267
+ raise IGRateLimitAccured()
268
+ wait_timeout += timeout_increment
269
+ logging.info("Waiting %d seconds according configuration option `IG_WAIT_TIMEOUT` with `IG_TIMEOUT_INCREMENT`", wait_timeout)
212
270
  if res:
213
271
  for i in res:
214
272
  if i["media_type"] == JobType.COLLECTION:
@@ -218,10 +276,72 @@ class InstagramScraper(ScraperAbstract):
218
276
  else:
219
277
  if os.path.exists(i["local_media_path"]):
220
278
  os.unlink(i["local_media_path"])
221
- os.unlink(INST_SESSION_FILE)
279
+ if os.path.exists(self.inst_session_file):
280
+ os.unlink(self.inst_session_file)
281
+ self.login()
222
282
  time.sleep(wait_timeout)
223
283
  except (MediaNotFound, ClientNotFoundError, UserNotFound) as e:
224
284
  raise NotFound(extract_exception_message(e))
225
285
  except IGUnknownError as e:
226
286
  raise UnknownError(extract_exception_message(e))
227
287
  return res
288
+
289
+ def email_challenge_resolver(self, username: str) -> Optional[str]:
290
+ logging.info("Started email challenge resolver")
291
+ mail = imaplib.IMAP4_SSL(self.account.get("imap_server", default="imap.bagrintsev.me"))
292
+ mail.login(self.account.get("imap_login", default=""), self.account.get("imap_password", default="")) # email server creds
293
+ mail.select("inbox")
294
+ _, data = mail.search(None, "(UNSEEN)")
295
+ ids = data.pop().split()
296
+ for num in reversed(ids):
297
+ mail.store(num, "+FLAGS", "\\Seen") # mark as read
298
+ _, data = mail.fetch(num, "(RFC822)")
299
+ msg = email.message_from_string(data[0][1].decode())
300
+ payloads = msg.get_payload()
301
+ if not isinstance(payloads, list):
302
+ payloads = [msg]
303
+ code = None
304
+ for payload in payloads:
305
+ body = ''
306
+ try:
307
+ body = payload.get_payload(decode=True).decode()
308
+ except:
309
+ continue
310
+ if "<div" not in body:
311
+ continue
312
+ match = re.search(">([^>]*?({u})[^<]*?)<".format(u=username), body)
313
+ if not match:
314
+ continue
315
+ logging.info("Match from email: '%s'", match.group(1))
316
+ match = re.search(r">(\d{6})<", body)
317
+ if not match:
318
+ logging.info('Skip this email, "code" not found')
319
+ continue
320
+ code = match.group(1)
321
+ if code:
322
+ logging.info("Found IG code at mail server: '%s'", code)
323
+ return code
324
+ return None
325
+
326
+ def get_code_from_sms(self, username: str) -> Optional[str]:
327
+ while True:
328
+ code = input(f"Enter code (6 digits) for {username}: ").strip()
329
+ if code and code.isdigit():
330
+ return code
331
+ return None
332
+
333
+ def challenge_code_handler(self, username: str, choice: ChallengeChoice) -> bool:
334
+ if choice == ChallengeChoice.SMS:
335
+ return False
336
+ #return self.get_code_from_sms(username)
337
+ elif choice == ChallengeChoice.EMAIL:
338
+ return self.email_challenge_resolver(username)
339
+
340
+ return False
341
+
342
+ def change_password_handler(self, username: str) -> str:
343
+ # Simple way to generate a random string
344
+ chars = list("abcdefghijklmnopqrstuvwxyz1234567890!&£@#")
345
+ password = "".join(random.sample(chars, 10))
346
+ logging.info("Generated new IG password: '%s'", password)
347
+ return password
@@ -4,7 +4,7 @@ import time
4
4
  import socket
5
5
  import ssl
6
6
 
7
- from abc import abstractmethod
7
+ #from abc import abstractmethod
8
8
  from typing import Callable, Union
9
9
 
10
10
  import json
@@ -78,10 +78,10 @@ def patched_fetch_bearer_token(self) -> None:
78
78
 
79
79
  class YoutubeAbstract(ScraperAbstract):
80
80
  DOWNLOAD_DIR = "/tmp"
81
- YT_SESSION_FILE = '/var/warp_beacon/yt_session.json'
81
+ YT_SESSION_FILE = '/var/warp_beacon/yt_session_%d.json'
82
82
 
83
- def __init__(self) -> None:
84
- pass
83
+ def __init__(self, account: tuple) -> None:
84
+ super().__init__(account)
85
85
 
86
86
  def __del__(self) -> None:
87
87
  pass
@@ -91,7 +91,7 @@ class YoutubeAbstract(ScraperAbstract):
91
91
  raise NameError("No file provided")
92
92
  path_info = pathlib.Path(filename)
93
93
  ext = path_info.suffix
94
- old_filename = path_info.stem
94
+ #old_filename = path_info.stem
95
95
  time_name = str(time.time()).replace('.', '_')
96
96
  new_filename = "%s%s" % (time_name, ext)
97
97
  new_filepath = "%s/%s" % (os.path.dirname(filename), new_filename)
@@ -170,11 +170,9 @@ class YoutubeAbstract(ScraperAbstract):
170
170
  InnerTube.fetch_bearer_token = patched_fetch_bearer_token
171
171
  _default_clients["ANDROID"]["innertube_context"]["context"]["client"]["clientVersion"] = "19.08.35"
172
172
  _default_clients["ANDROID_MUSIC"] = _default_clients["ANDROID"]
173
- return YouTube(
174
- url=url,
175
- #client="WEB",
176
- use_oauth=True,
177
- allow_oauth_cache=True,
178
- token_file=self.YT_SESSION_FILE,
179
- on_progress_callback = self.yt_on_progress
180
- )
173
+ yt_opts = {"url": url, "on_progress_callback": self.yt_on_progress}
174
+ #yt_opts["client"] = "WEB"
175
+ yt_opts["use_oauth"] = True
176
+ yt_opts["allow_oauth_cache"] = True
177
+ yt_opts["token_file"] = self.YT_SESSION_FILE % self.account_index
178
+ return YouTube(**yt_opts)
@@ -2,8 +2,6 @@ from warp_beacon.jobs.types import JobType
2
2
  from warp_beacon.scraper.youtube.abstract import YoutubeAbstract
3
3
  from warp_beacon.scraper.exceptions import NotFound
4
4
 
5
- from pytubefix import YouTube
6
-
7
5
  import logging
8
6
 
9
7
  class YoutubeMusicScraper(YoutubeAbstract):
@@ -2,8 +2,6 @@ from warp_beacon.jobs.types import JobType
2
2
  from warp_beacon.scraper.youtube.abstract import YoutubeAbstract
3
3
  from warp_beacon.scraper.exceptions import NotFound
4
4
 
5
- from pytubefix import YouTube
6
-
7
5
  import logging
8
6
 
9
7
  class YoutubeShortsScraper(YoutubeAbstract):
@@ -2,7 +2,6 @@ from warp_beacon.jobs.types import JobType
2
2
  from warp_beacon.scraper.youtube.abstract import YoutubeAbstract
3
3
  from warp_beacon.scraper.exceptions import YotubeLiveError, NotFound, YotubeAgeRestrictedError
4
4
 
5
- from pytubefix import YouTube
6
5
  from pytubefix.exceptions import AgeRestrictedError
7
6
 
8
7
  import logging
@@ -50,7 +50,7 @@ class Bot(object):
50
50
  bot_token=tg_token,
51
51
  api_id=tg_api_id,
52
52
  api_hash=tg_api_hash,
53
- workdir='/',
53
+ workdir='/var/warp_beacon',
54
54
  workers=int(os.environ.get("TG_WORKERS_POOL_SIZE", default=workers_amount))
55
55
  )
56
56
 
@@ -373,4 +373,4 @@ class Bot(object):
373
373
  finally:
374
374
  job.remove_files()
375
375
 
376
- return tg_file_ids
376
+ return tg_file_ids
@@ -1,9 +1,9 @@
1
- import re
1
+ from typing import Union
2
2
 
3
+ import re
3
4
  import requests
4
5
 
5
- from enum import Enum
6
- from typing import Union
6
+ from pyrogram.types import Message
7
7
 
8
8
  from warp_beacon.jobs import Origin
9
9
  from warp_beacon.jobs.types import JobType
@@ -14,7 +14,7 @@ class Utils(object):
14
14
  expected_patronum_compiled_re = re.compile(r'Expected ([A-Z]+), got ([A-Z]+) file id instead')
15
15
 
16
16
  @staticmethod
17
- def extract_file_id(message: "Message") -> Union[None, str]:
17
+ def extract_file_id(message: Message) -> Union[None, str]:
18
18
  possible_attrs = ("video", "photo", "audio", "animation", "document")
19
19
  for attr in possible_attrs:
20
20
  if hasattr(message, attr):
@@ -73,10 +73,17 @@ class Utils(object):
73
73
  return (seq[pos:pos + size] for pos in range(0, len(seq), size))
74
74
 
75
75
  @staticmethod
76
- def extract_message_text(message: "Message") -> str:
76
+ def extract_message_text(message: Message) -> str:
77
77
  if hasattr(message, "text") and message.text:
78
78
  return message.text
79
79
  if hasattr(message, "caption") and message.caption:
80
80
  return message.caption
81
81
 
82
82
  return ''
83
+
84
+ @staticmethod
85
+ def extract_message_author(message: Message) -> str:
86
+ if message.from_user:
87
+ return message.from_user
88
+ return ''
89
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: warp_beacon
3
- Version: 2.0.10
3
+ Version: 2.1.1
4
4
  Summary: Telegram bot for expanding external media links
5
5
  Home-page: https://github.com/sb0y/warp_beacon
6
6
  Author: Andrey Bagrintsev
@@ -230,7 +230,7 @@ Requires-Dist: tgcrypto
230
230
  Requires-Dist: pyrogram
231
231
  Requires-Dist: pillow-heif
232
232
  Requires-Dist: pytubefix
233
- Requires-Dist: av
233
+ Requires-Dist: av ==12.3.0
234
234
  Requires-Dist: urlextract
235
235
  Requires-Dist: pillow
236
236
  Requires-Dist: pymongo
@@ -0,0 +1,43 @@
1
+ etc/warp_beacon/warp_beacon.conf,sha256=pEhCjw6VBCN4c61rTJ4RwOKxAggpy0VvTcgObXFZpaI,318
2
+ lib/systemd/system/warp_beacon.service,sha256=lPmHqLqcI2eIV7nwHS0qcALQrznixqJuwwPfa2mDLUA,372
3
+ var/warp_beacon/accounts.json,sha256=rKFQM_b9eoDS4mJ1B_SZNolPLXx1SQdQMdY2F_ZcBt8,1523
4
+ var/warp_beacon/placeholder.gif,sha256=cE5CGJVaop4Sx21zx6j4AyoHU0ncmvQuS2o6hJfEH88,6064
5
+ warp_beacon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ warp_beacon/__version__.py,sha256=0npkOCld8bseamcx2JJABJTYUri9sDUBFFPsagH-KOs,23
7
+ warp_beacon/warp_beacon.py,sha256=7KEtZDj-pdhtl6m-zFLsSojs1ZR4o7L0xbqtdmYPvfE,342
8
+ warp_beacon/compress/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ warp_beacon/compress/video.py,sha256=_PDMVYCyzLYxHv1uZmmzGcG_8rjaZr7BTXsXTTy_oS4,2846
10
+ warp_beacon/jobs/__init__.py,sha256=ED8_tPle4iL4kqNW0apAVkgNQtRRTnYfAJwBjO1g0JY,180
11
+ warp_beacon/jobs/abstract.py,sha256=fXdYgJcCJhsuxf_4nXzsA9cZt_dGbTr-tjvbzs_m_vQ,2631
12
+ warp_beacon/jobs/download_job.py,sha256=5HiPcnJppFMhO14___3eSkoMygM3y-vhpGkMAuNhK7s,854
13
+ warp_beacon/jobs/types.py,sha256=Ae8zINgbs7cOcYkYoOCOACA7duyhnIGMQAJ_SJB1QRQ,176
14
+ warp_beacon/jobs/upload_job.py,sha256=_ul4psPej1jLEs-BMcMR80GbXDSmm38jE9yoZtecclY,741
15
+ warp_beacon/mediainfo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ warp_beacon/mediainfo/abstract.py,sha256=ZR2JMuRpoh7nDNov9a8YkAfr6BI2HXnXzQtVrLgDxjs,1185
17
+ warp_beacon/mediainfo/audio.py,sha256=ous88kwQj4bDIChN5wnGil5LqTs0IQHH0d-nyrL0-ZM,651
18
+ warp_beacon/mediainfo/silencer.py,sha256=MgUc9Ibbhjhg9GbJMNfJqrdDkMsQShZkQ1sCwvW_-qI,1647
19
+ warp_beacon/mediainfo/video.py,sha256=AIRy_op_BvehsjarM1rvT5Qo0QWwf-Q6xVVd_aCnbJ4,2505
20
+ warp_beacon/scraper/__init__.py,sha256=QZOyrPh5PTgFh70Qqh629RPWi4Ed-kNipiwsSlS7Mnk,12791
21
+ warp_beacon/scraper/abstract.py,sha256=aNZ9ypF9B8BjflcIwi-7wEzIqF-XPeF0xvfX9CP_iIw,2708
22
+ warp_beacon/scraper/account_selector.py,sha256=SbDO7W5WUCmMDokhuSx0yfEQiu5TW9UumwScGHsaGk8,2633
23
+ warp_beacon/scraper/exceptions.py,sha256=5A1viJm8sT_ZcI1IccAxQ9O_eYpJNHt3TmYLM0P22Q8,1238
24
+ warp_beacon/scraper/instagram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ warp_beacon/scraper/instagram/instagram.py,sha256=MNfYOMYzaVG65wrtL63oQu6BTq6i_MuzjBxsoEMlFYA,13327
26
+ warp_beacon/scraper/youtube/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ warp_beacon/scraper/youtube/abstract.py,sha256=Fp_NL8Y5j5Tk9_d5QDfkCQZva7I0JC_1B26RhorMjtw,6065
28
+ warp_beacon/scraper/youtube/music.py,sha256=8TguqXc1BC9Slp09BzCU8dtQ6BnyKwzZSLLFHpqDHtg,1498
29
+ warp_beacon/scraper/youtube/shorts.py,sha256=pHfvEBau8Zp7Ar3LBuPmjqYq8fmjJUQvzeZXujQMpG4,1203
30
+ warp_beacon/scraper/youtube/youtube.py,sha256=K98n2TSJaDZt-xT7mADZL1UEf2exIYm0Wnenn2GAYfI,2250
31
+ warp_beacon/storage/__init__.py,sha256=8XsJXq9X7GDlTaWREF4W1PDX9PH5utwhjf5c5M8Bb7o,3378
32
+ warp_beacon/telegram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ warp_beacon/telegram/bot.py,sha256=Y3QT5iIBdM6ILgN832asCbzr2K0v0hYK7IxwV7aC48M,13100
34
+ warp_beacon/telegram/handlers.py,sha256=MTcHZmWe8RAcZdicnqQewy_SkwujhnaoqJgWHpebfVs,6350
35
+ warp_beacon/telegram/placeholder_message.py,sha256=u5kVfTjGmVYkwA5opniRltHXGpsdSxI41WEde8J5os0,6418
36
+ warp_beacon/telegram/utils.py,sha256=LdCU4ChJHyzpCvyG5v3XcUtUgK3v5by_v8D56VsPeI0,2171
37
+ warp_beacon/uploader/__init__.py,sha256=qODBuIvWtypQQyKl3Y0QiBXC5SipVFL4b64D91EmTyw,4764
38
+ warp_beacon-2.1.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
39
+ warp_beacon-2.1.1.dist-info/METADATA,sha256=oiDULqyo3Qu0FxBilngJEN9xvhKqHVEir6Xp7Tz7ojs,21250
40
+ warp_beacon-2.1.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
41
+ warp_beacon-2.1.1.dist-info/entry_points.txt,sha256=eSB61Rb89d56WY0O-vEIQwkn18J-4CMrJcLA_R_8h3g,119
42
+ warp_beacon-2.1.1.dist-info/top_level.txt,sha256=SIMha34qjGXaijMgtzESgQ-Cu-KbLHbmQacwue_ggTY,917
43
+ warp_beacon-2.1.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.2.0)
2
+ Generator: setuptools (75.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -13,8 +13,10 @@ warp_beacon/mediainfo/silencer
13
13
  warp_beacon/mediainfo/video
14
14
  warp_beacon/scraper
15
15
  warp_beacon/scraper/abstract
16
+ warp_beacon/scraper/account_selector
16
17
  warp_beacon/scraper/exceptions
17
18
  warp_beacon/scraper/instagram
19
+ warp_beacon/scraper/instagram/instagram
18
20
  warp_beacon/scraper/types
19
21
  warp_beacon/scraper/youtube
20
22
  warp_beacon/scraper/youtube/abstract
@@ -1,40 +0,0 @@
1
- etc/warp_beacon/warp_beacon.conf,sha256=-QyHfsyo-1Ukxlsz3KSE2vKvLCRm5NtA36_jmfqSAsI,307
2
- lib/systemd/system/warp_beacon.service,sha256=lPmHqLqcI2eIV7nwHS0qcALQrznixqJuwwPfa2mDLUA,372
3
- var/warp_beacon/placeholder.gif,sha256=cE5CGJVaop4Sx21zx6j4AyoHU0ncmvQuS2o6hJfEH88,6064
4
- warp_beacon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- warp_beacon/__version__.py,sha256=vUAwymcHllnRpWKcmq_zCPb9Ro_Z8h2NwtbWgfkmsVs,24
6
- warp_beacon/warp_beacon.py,sha256=7KEtZDj-pdhtl6m-zFLsSojs1ZR4o7L0xbqtdmYPvfE,342
7
- warp_beacon/compress/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- warp_beacon/compress/video.py,sha256=_PDMVYCyzLYxHv1uZmmzGcG_8rjaZr7BTXsXTTy_oS4,2846
9
- warp_beacon/jobs/__init__.py,sha256=ED8_tPle4iL4kqNW0apAVkgNQtRRTnYfAJwBjO1g0JY,180
10
- warp_beacon/jobs/abstract.py,sha256=NCwsogk3-m8F6a-SeiY4CEdTgqJn2V_MU4H7_iYuF7k,2463
11
- warp_beacon/jobs/download_job.py,sha256=5HiPcnJppFMhO14___3eSkoMygM3y-vhpGkMAuNhK7s,854
12
- warp_beacon/jobs/types.py,sha256=Ae8zINgbs7cOcYkYoOCOACA7duyhnIGMQAJ_SJB1QRQ,176
13
- warp_beacon/jobs/upload_job.py,sha256=_ul4psPej1jLEs-BMcMR80GbXDSmm38jE9yoZtecclY,741
14
- warp_beacon/mediainfo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- warp_beacon/mediainfo/abstract.py,sha256=ZR2JMuRpoh7nDNov9a8YkAfr6BI2HXnXzQtVrLgDxjs,1185
16
- warp_beacon/mediainfo/audio.py,sha256=ous88kwQj4bDIChN5wnGil5LqTs0IQHH0d-nyrL0-ZM,651
17
- warp_beacon/mediainfo/silencer.py,sha256=MgUc9Ibbhjhg9GbJMNfJqrdDkMsQShZkQ1sCwvW_-qI,1647
18
- warp_beacon/mediainfo/video.py,sha256=AIRy_op_BvehsjarM1rvT5Qo0QWwf-Q6xVVd_aCnbJ4,2505
19
- warp_beacon/scraper/__init__.py,sha256=3pwcnYp4fdGG_zdnKGryfbtKJWHOZXuUOTSiCB3mvdQ,11049
20
- warp_beacon/scraper/abstract.py,sha256=-yMCEW2JUQWUVs4TbiUi6PEw8RyRuTvrJRIUeEpASP0,1723
21
- warp_beacon/scraper/exceptions.py,sha256=j3sUi3LT3l-mqW8X6sNSpmx5JIioFODWlOZW8I9RJHA,1191
22
- warp_beacon/scraper/instagram.py,sha256=SHyyNvy5dIPhKllIBEvUhM6yq2zeOw4V6r1cvP69NwU,8580
23
- warp_beacon/scraper/youtube/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- warp_beacon/scraper/youtube/abstract.py,sha256=ZbCaYAsGowGsrbJDY39Yk_JAhETncw9aE9GYA9sPXTA,5940
25
- warp_beacon/scraper/youtube/music.py,sha256=Cf7QNgers2zo-cQRxi__NPz7V8ZoWCC-g9b8yZNfJ4E,1529
26
- warp_beacon/scraper/youtube/shorts.py,sha256=vMEshUBtFwchwp_W2uUoPKXwgll2wFgKdIfvSFKXxLE,1234
27
- warp_beacon/scraper/youtube/youtube.py,sha256=TmKMdGbhORTBcBYB6V4ItvWKnxWtUfMbXhomonsmAso,2280
28
- warp_beacon/storage/__init__.py,sha256=8XsJXq9X7GDlTaWREF4W1PDX9PH5utwhjf5c5M8Bb7o,3378
29
- warp_beacon/telegram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- warp_beacon/telegram/bot.py,sha256=HGsc1H_CxiE0GBxELqATpSWIjahwjU8FuH0geQOruAI,13086
31
- warp_beacon/telegram/handlers.py,sha256=MTcHZmWe8RAcZdicnqQewy_SkwujhnaoqJgWHpebfVs,6350
32
- warp_beacon/telegram/placeholder_message.py,sha256=u5kVfTjGmVYkwA5opniRltHXGpsdSxI41WEde8J5os0,6418
33
- warp_beacon/telegram/utils.py,sha256=1tm_DH1F2snDxSqwZnKD4ijvTrobv_kscgt3w-bWa6g,2027
34
- warp_beacon/uploader/__init__.py,sha256=qODBuIvWtypQQyKl3Y0QiBXC5SipVFL4b64D91EmTyw,4764
35
- warp_beacon-2.0.10.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
36
- warp_beacon-2.0.10.dist-info/METADATA,sha256=nsFNDX6H3CtUbCBnDu5E2Wlec9v4FePl6mSQMqp_544,21242
37
- warp_beacon-2.0.10.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
38
- warp_beacon-2.0.10.dist-info/entry_points.txt,sha256=eSB61Rb89d56WY0O-vEIQwkn18J-4CMrJcLA_R_8h3g,119
39
- warp_beacon-2.0.10.dist-info/top_level.txt,sha256=pu6xG8OO_nCGllnOfAZ6QpVfivtmHVxPlYK8SZzUDqA,840
40
- warp_beacon-2.0.10.dist-info/RECORD,,