warp-beacon 2.0.6__tar.gz → 2.0.8__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 (51) hide show
  1. {warp_beacon-2.0.6/warp_beacon.egg-info → warp_beacon-2.0.8}/PKG-INFO +1 -1
  2. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/etc/warp_beacon.conf +2 -1
  3. warp_beacon-2.0.8/warp_beacon/__version__.py +2 -0
  4. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/jobs/abstract.py +4 -0
  5. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/mediainfo/silencer.py +1 -1
  6. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/scraper/__init__.py +24 -4
  7. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/scraper/abstract.py +3 -0
  8. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/scraper/exceptions.py +3 -0
  9. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/scraper/instagram.py +1 -0
  10. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/scraper/youtube/abstract.py +78 -3
  11. warp_beacon-2.0.8/warp_beacon/scraper/youtube/music.py +53 -0
  12. warp_beacon-2.0.8/warp_beacon/scraper/youtube/shorts.py +46 -0
  13. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/scraper/youtube/youtube.py +31 -25
  14. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/telegram/bot.py +27 -1
  15. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/telegram/handlers.py +11 -2
  16. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/uploader/__init__.py +7 -1
  17. {warp_beacon-2.0.6 → warp_beacon-2.0.8/warp_beacon.egg-info}/PKG-INFO +1 -1
  18. warp_beacon-2.0.6/warp_beacon/__version__.py +0 -2
  19. warp_beacon-2.0.6/warp_beacon/scraper/youtube/music.py +0 -47
  20. warp_beacon-2.0.6/warp_beacon/scraper/youtube/shorts.py +0 -42
  21. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/LICENSE +0 -0
  22. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/MANIFEST.in +0 -0
  23. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/README.md +0 -0
  24. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/assets/placeholder.gif +0 -0
  25. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/etc/.gitignore +0 -0
  26. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/etc/warp_beacon.service +0 -0
  27. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/pyproject.toml +0 -0
  28. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/setup.cfg +0 -0
  29. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/setup.py +0 -0
  30. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/__init__.py +0 -0
  31. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/compress/__init__.py +0 -0
  32. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/compress/video.py +0 -0
  33. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/jobs/__init__.py +0 -0
  34. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/jobs/download_job.py +0 -0
  35. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/jobs/types.py +0 -0
  36. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/jobs/upload_job.py +0 -0
  37. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/mediainfo/__init__.py +0 -0
  38. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/mediainfo/abstract.py +0 -0
  39. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/mediainfo/audio.py +0 -0
  40. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/mediainfo/video.py +0 -0
  41. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/scraper/youtube/__init__.py +0 -0
  42. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/storage/__init__.py +0 -0
  43. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/telegram/__init__.py +0 -0
  44. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/telegram/placeholder_message.py +0 -0
  45. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/telegram/utils.py +0 -0
  46. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon/warp_beacon.py +0 -0
  47. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon.egg-info/SOURCES.txt +0 -0
  48. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon.egg-info/dependency_links.txt +0 -0
  49. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon.egg-info/entry_points.txt +0 -0
  50. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon.egg-info/requires.txt +0 -0
  51. {warp_beacon-2.0.6 → warp_beacon-2.0.8}/warp_beacon.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: warp_beacon
3
- Version: 2.0.6
3
+ Version: 2.0.8
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
@@ -2,6 +2,7 @@ TG_TOKEN=""
2
2
  TG_API_ID=""
3
3
  TG_API_HASH=""
4
4
  TG_BOT_NAME=""
5
+ TG_BOT_ADMIN_USERNAME=""
5
6
  INSTAGRAM_LOGIN=""
6
7
  INSTAGRAM_PASSWORD=""
7
8
  INSTAGRAM_VERIFICATION_CODE=""
@@ -10,4 +11,4 @@ MONGODB_PORT="27017"
10
11
  MONGODB_USER="root"
11
12
  MONGODB_PASSWORD="changeme"
12
13
  VIDEO_STORAGE_DIR="/var/warp_beacon/videos"
13
- ENABLE_DONATES=true
14
+ ENABLE_DONATES=true
@@ -0,0 +1,2 @@
1
+ __version__ = "2.0.8"
2
+
@@ -29,6 +29,8 @@ class JobSettings(TypedDict):
29
29
  media_collection: list
30
30
  job_origin: Origin
31
31
  canonical_name: str
32
+ is_message_to_admin: bool
33
+ message_text: str
32
34
 
33
35
  class AbstractJob(ABC):
34
36
  job_id: uuid.UUID = None
@@ -52,6 +54,8 @@ class AbstractJob(ABC):
52
54
  media_collection: list = []
53
55
  job_origin: Origin = Origin.UNKNOWN
54
56
  canonical_name: str = ""
57
+ is_message_to_admin: bool = False
58
+ message_text: str = ""
55
59
 
56
60
  def __init__(self, **kwargs: Unpack[JobSettings]) -> None:
57
61
  if kwargs:
@@ -28,7 +28,7 @@ class Silencer(VideoInfo):
28
28
  if packet:
29
29
  out_container.mux(packet)
30
30
  #
31
- aframe = av.AudioFrame(samples=32, format='s16')
31
+ aframe = av.AudioFrame(samples=64, format='s16')
32
32
  aframe.pts = frame.pts
33
33
  aframe.sample_rate = 16000
34
34
  aframe.rate = 44100
@@ -1,11 +1,12 @@
1
1
  import os
2
2
  import time
3
+ import asyncio
3
4
 
4
5
  from typing import Optional
5
6
  import multiprocessing
6
7
  from queue import Empty
7
8
 
8
- from warp_beacon.scraper.exceptions import NotFound, UnknownError, TimeOut, Unavailable, FileTooBig, YotubeLiveError
9
+ from warp_beacon.scraper.exceptions import NotFound, UnknownError, TimeOut, Unavailable, FileTooBig, YotubeLiveError, YotubeAgeRestrictedError
9
10
  from warp_beacon.mediainfo.video import VideoInfo
10
11
  from warp_beacon.mediainfo.audio import AudioInfo
11
12
  from warp_beacon.mediainfo.silencer import Silencer
@@ -13,6 +14,7 @@ from warp_beacon.compress.video import VideoCompress
13
14
  from warp_beacon.uploader import AsyncUploader
14
15
  from warp_beacon.jobs import Origin
15
16
  from warp_beacon.jobs.download_job import DownloadJob
17
+ from warp_beacon.jobs.upload_job import UploadJob
16
18
  from warp_beacon.jobs.types import JobType
17
19
 
18
20
  import logging
@@ -24,6 +26,7 @@ class AsyncDownloader(object):
24
26
  job_queue = multiprocessing.Queue()
25
27
  uploader = None
26
28
  workers_count = 0
29
+ auth_event = multiprocessing.Event()
27
30
 
28
31
  def __init__(self, uploader: AsyncUploader, workers_count: int) -> None:
29
32
  self.allow_loop = multiprocessing.Value('i', 1)
@@ -86,6 +89,9 @@ class AsyncDownloader(object):
86
89
  elif job.job_origin is Origin.YOUTUBE:
87
90
  from warp_beacon.scraper.youtube.youtube import YoutubeScraper
88
91
  actor = YoutubeScraper()
92
+ #self.auth_event = multiprocessing.Event()
93
+ actor.send_message_to_admin_func = self.send_message_to_admin
94
+ actor.auth_event = self.auth_event
89
95
  while True:
90
96
  try:
91
97
  logging.info("Downloading URL '%s'", job.url)
@@ -123,6 +129,14 @@ class AsyncDownloader(object):
123
129
  job_failed_msg="Youtube Live videos are not supported. Please wait until the live broadcast ends.")
124
130
  )
125
131
  break
132
+ except YotubeAgeRestrictedError as e:
133
+ logging.error("Youtube Age Restricted error")
134
+ logging.exception(e)
135
+ self.uploader.queue_task(job.to_upload_job(
136
+ job_failed=True,
137
+ job_failed_msg="Youtube Age Restricted error. Check your bot Youtube account settings.")
138
+ )
139
+ break
126
140
  except (UnknownError, Exception) as e:
127
141
  logging.warning("UnknownError occurred!")
128
142
  logging.exception(e)
@@ -139,7 +153,7 @@ class AsyncDownloader(object):
139
153
  break
140
154
  self.uploader.queue_task(job.to_upload_job(
141
155
  job_failed=True,
142
- job_failed_msg="WOW, unknown error occured! Please send service logs to developer via email: andrey@bagrintsev.me.")
156
+ job_failed_msg="WOW, unknown error occured! Please [create issue](https://github.com/sb0y/warp_beacon/issues) with service logs.")
143
157
  )
144
158
  break
145
159
 
@@ -182,7 +196,7 @@ class AsyncDownloader(object):
182
196
  if not v["media_info"]["has_sound"]:
183
197
  silencer = Silencer(v["local_media_path"])
184
198
  silent_video_path = silencer.add_silent_audio()
185
- os.unlink(j["local_media_path"])
199
+ os.unlink(v["local_media_path"])
186
200
  v["local_media_path"] = silent_video_path
187
201
  v["media_info"].update(silencer.get_finfo())
188
202
  v["media_info"]["has_sound"] = True
@@ -242,4 +256,10 @@ class AsyncDownloader(object):
242
256
  return str(job.job_id)
243
257
 
244
258
  def notify_task_failed(self, job: DownloadJob) -> None:
245
- self.uploader.queue_task(job.to_upload_job(job_failed=True))
259
+ self.uploader.queue_task(job.to_upload_job(job_failed=True))
260
+
261
+ def send_message_to_admin(self, text: str) -> None:
262
+ self.uploader.queue_task(UploadJob.build(
263
+ is_message_to_admin=True,
264
+ message_text=text
265
+ ))
@@ -10,6 +10,9 @@ from pillow_heif import register_heif_opener
10
10
  import logging
11
11
 
12
12
  class ScraperAbstract(ABC):
13
+ send_message_to_admin_func = None
14
+ auth_event = None
15
+
13
16
  def __init__(self) -> None:
14
17
  pass
15
18
 
@@ -31,6 +31,9 @@ class Unavailable(ScraperError):
31
31
  class YotubeLiveError(ScraperError):
32
32
  pass
33
33
 
34
+ class YotubeAgeRestrictedError(ScraperError):
35
+ pass
36
+
34
37
  class UnknownError(ScraperError):
35
38
  pass
36
39
 
@@ -145,6 +145,7 @@ class InstagramScraper(ScraperAbstract):
145
145
  effective_url = "https://www.instagram.com/stories/%s/%s/" % (story_info.user.username, effective_story_id)
146
146
  if story_info.media_type == 1: # photo
147
147
  path = str(self._download_hndlr(self.cl.story_download_by_url, url=story_info.thumbnail_url, folder='/tmp'))
148
+ path_lowered = path.lower()
148
149
  if ".webp" in path_lowered:
149
150
  path = InstagramScraper.convert_webp_to_png(path)
150
151
  if ".heic" in path_lowered:
@@ -7,6 +7,8 @@ import ssl
7
7
  from abc import abstractmethod
8
8
  from typing import Callable, Union
9
9
 
10
+ import json
11
+
10
12
  import requests
11
13
  import urllib
12
14
  import http.client
@@ -17,12 +19,66 @@ from warp_beacon.scraper.abstract import ScraperAbstract
17
19
  from warp_beacon.mediainfo.abstract import MediaInfoAbstract
18
20
  from warp_beacon.scraper.exceptions import NotFound, UnknownError, TimeOut, Unavailable, extract_exception_message
19
21
 
22
+ from pytubefix import YouTube
23
+ from pytubefix.innertube import _default_clients
24
+ from pytubefix.streams import Stream
25
+ from pytubefix.innertube import InnerTube, _client_id, _client_secret
20
26
  from pytubefix.exceptions import VideoUnavailable, VideoPrivate, MaxRetriesExceeded
27
+ from pytubefix import request
21
28
 
22
29
  import logging
23
30
 
31
+ def patched_fetch_bearer_token(self) -> None:
32
+ """Fetch an OAuth token."""
33
+ # Subtracting 30 seconds is arbitrary to avoid potential time discrepencies
34
+ start_time = int(time.time() - 30)
35
+ data = {
36
+ 'client_id': _client_id,
37
+ 'scope': 'https://www.googleapis.com/auth/youtube'
38
+ }
39
+ response = request._execute_request(
40
+ 'https://oauth2.googleapis.com/device/code',
41
+ 'POST',
42
+ headers={
43
+ 'Content-Type': 'application/json'
44
+ },
45
+ data=data
46
+ )
47
+ response_data = json.loads(response.read())
48
+ verification_url = response_data['verification_url']
49
+ user_code = response_data['user_code']
50
+
51
+ logging.warning("Please open %s and input code '%s'", verification_url, user_code)
52
+ self.send_message_to_admin_func(
53
+ f"Please open {verification_url} and input code `{user_code}`.\n\n"
54
+ "Please select a Google account with verified age.\n"
55
+ "This will allow you to avoid error the **AgeRestrictedError** when accessing some content.")
56
+ self.auth_event.wait()
57
+
58
+ data = {
59
+ 'client_id': _client_id,
60
+ 'client_secret': _client_secret,
61
+ 'device_code': response_data['device_code'],
62
+ 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code'
63
+ }
64
+ response = request._execute_request(
65
+ 'https://oauth2.googleapis.com/token',
66
+ 'POST',
67
+ headers={
68
+ 'Content-Type': 'application/json'
69
+ },
70
+ data=data
71
+ )
72
+ response_data = json.loads(response.read())
73
+
74
+ self.access_token = response_data['access_token']
75
+ self.refresh_token = response_data['refresh_token']
76
+ self.expires = start_time + response_data['expires_in']
77
+ self.cache_tokens()
78
+
24
79
  class YoutubeAbstract(ScraperAbstract):
25
80
  DOWNLOAD_DIR = "/tmp"
81
+ YT_SESSION_FILE = '/var/warp_beacon/yt_session.json'
26
82
 
27
83
  def __init__(self) -> None:
28
84
  pass
@@ -49,9 +105,9 @@ class YoutubeAbstract(ScraperAbstract):
49
105
  if "yt_download_" in i:
50
106
  os.unlink("%s/%s" % (self.DOWNLOAD_DIR, i))
51
107
 
52
- def download_thumbnail(self, url: str) -> Union[io.BytesIO, None]:
108
+ def download_thumbnail(self, url: str, timeout: int) -> Union[io.BytesIO, None]:
53
109
  try:
54
- reply = requests.get(url, stream=True)
110
+ reply = requests.get(url, timeout=(timeout, timeout))
55
111
  if reply.ok and reply.status_code == 200:
56
112
  image = Image.open(io.BytesIO(reply.content))
57
113
  image = MediaInfoAbstract.shrink_image_to_fit(image)
@@ -65,7 +121,7 @@ class YoutubeAbstract(ScraperAbstract):
65
121
 
66
122
  return None
67
123
 
68
- def _download_hndlr(self, func: Callable, *args: tuple[str], **kwargs: dict[str]) -> Union[str, dict]:
124
+ def _download_hndlr(self, func: Callable, *args: tuple[str], **kwargs: dict[str]) -> Union[str, dict, io.BytesIO]:
69
125
  ret_val = ''
70
126
  max_retries = int(os.environ.get("YT_MAX_RETRIES", default=self.YT_MAX_RETRIES_DEFAULT))
71
127
  pause_secs = int(os.environ.get("YT_PAUSE_BEFORE_RETRY", default=self.YT_PAUSE_BEFORE_RETRY_DEFAULT))
@@ -103,3 +159,22 @@ class YoutubeAbstract(ScraperAbstract):
103
159
  raise Unavailable(extract_exception_message(e))
104
160
 
105
161
  return ret_val
162
+
163
+ def yt_on_progress(self, stream: Stream, chunk: bytes, bytes_remaining: int) -> None:
164
+ pass
165
+ #logging.info("bytes: %d, bytes remaining: %d", chunk, bytes_remaining)
166
+
167
+ def build_yt(self, url: str) -> YouTube:
168
+ InnerTube.send_message_to_admin_func = self.send_message_to_admin_func
169
+ InnerTube.auth_event = self.auth_event
170
+ InnerTube.fetch_bearer_token = patched_fetch_bearer_token
171
+ _default_clients["ANDROID"]["innertube_context"]["context"]["client"]["clientVersion"] = "19.08.35"
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
+ )
@@ -0,0 +1,53 @@
1
+ from warp_beacon.jobs.types import JobType
2
+ from warp_beacon.scraper.youtube.abstract import YoutubeAbstract
3
+ from warp_beacon.scraper.exceptions import NotFound
4
+
5
+ from pytubefix import YouTube
6
+
7
+ import logging
8
+
9
+ class YoutubeMusicScraper(YoutubeAbstract):
10
+ YT_MAX_RETRIES_DEFAULT = 6
11
+ YT_PAUSE_BEFORE_RETRY_DEFAULT = 3
12
+ YT_TIMEOUT_DEFAULT = 2
13
+ YT_TIMEOUT_INCREMENT_DEFAULT = 60
14
+
15
+ def _download(self, url: str, timeout: int = 0) -> list:
16
+ res = []
17
+ thumbnail = None
18
+ yt = self.build_yt(url)
19
+
20
+ if yt and yt.thumbnail_url:
21
+ thumbnail = self._download_hndlr(self.download_thumbnail, yt.thumbnail_url)
22
+
23
+ stream = yt.streams.get_audio_only()
24
+
25
+ if not stream:
26
+ raise NotFound("No suitable audio stream found")
27
+
28
+ logging.info("Announced audio file size: '%d'", stream.filesize)
29
+ if stream.filesize > 2e+9:
30
+ logging.warning("Downloading size reported by YouTube is over than 2 GB!")
31
+ raise FileTooBig("YouTube file is larger than 2 GB")
32
+ logging.info("Operation timeout is '%d'", timeout)
33
+ local_file = stream.download(
34
+ output_path=self.DOWNLOAD_DIR,
35
+ max_retries=0,
36
+ timeout=timeout,
37
+ skip_existing=False,
38
+ filename_prefix='yt_download_',
39
+ mp3=True
40
+ )
41
+ logging.debug("Temp filename: '%s'", local_file)
42
+ res.append({
43
+ "local_media_path": self.rename_local_file(local_file),
44
+ "performer": yt.author,
45
+ "thumb": thumbnail,
46
+ "canonical_name": stream.title,
47
+ "media_type": JobType.AUDIO
48
+ })
49
+
50
+ return res
51
+
52
+ def download(self, url: str) -> list:
53
+ return self._download_hndlr(self._download, url)
@@ -0,0 +1,46 @@
1
+ from warp_beacon.jobs.types import JobType
2
+ from warp_beacon.scraper.youtube.abstract import YoutubeAbstract
3
+ from warp_beacon.scraper.exceptions import NotFound
4
+
5
+ from pytubefix import YouTube
6
+
7
+ import logging
8
+
9
+ class YoutubeShortsScraper(YoutubeAbstract):
10
+ YT_MAX_RETRIES_DEFAULT = 8
11
+ YT_PAUSE_BEFORE_RETRY_DEFAULT = 3
12
+ YT_TIMEOUT_DEFAULT = 2
13
+ YT_TIMEOUT_INCREMENT_DEFAULT = 60
14
+
15
+ def _download(self, url: str, timeout: int = 0) -> list:
16
+ res = []
17
+ thumbnail = None
18
+ yt = self.build_yt(url)
19
+ stream = yt.streams.get_highest_resolution()
20
+
21
+ if not stream:
22
+ raise NotFound("No suitable video stream found")
23
+
24
+ if yt and yt.thumbnail_url:
25
+ thumbnail = self._download_hndlr(self.download_thumbnail, yt.thumbnail_url)
26
+
27
+ local_file = stream.download(
28
+ output_path=self.DOWNLOAD_DIR,
29
+ max_retries=0,
30
+ timeout=timeout,
31
+ skip_existing=False,
32
+ filename_prefix="yt_download_"
33
+ )
34
+ logging.debug("Temp filename: '%s'", local_file)
35
+ res.append({
36
+ "local_media_path": self.rename_local_file(local_file),
37
+ "performer": yt.author,
38
+ "thumb": thumbnail,
39
+ "canonical_name": stream.title,
40
+ "media_type": JobType.VIDEO
41
+ })
42
+
43
+ return res
44
+
45
+ def download(self, url: str) -> list:
46
+ return self._download_hndlr(self._download, url)
@@ -1,8 +1,9 @@
1
1
  from warp_beacon.jobs.types import JobType
2
2
  from warp_beacon.scraper.youtube.abstract import YoutubeAbstract
3
- from warp_beacon.scraper.exceptions import YotubeLiveError, NotFound
3
+ from warp_beacon.scraper.exceptions import YotubeLiveError, NotFound, YotubeAgeRestrictedError
4
4
 
5
5
  from pytubefix import YouTube
6
+ from pytubefix.exceptions import AgeRestrictedError
6
7
 
7
8
  import logging
8
9
 
@@ -30,35 +31,40 @@ class YoutubeScraper(YoutubeAbstract):
30
31
 
31
32
  def _download(self, url: str, timeout: int = 0) -> list:
32
33
  res = []
33
- thumbnail = None
34
- yt = YouTube(url)
34
+ try:
35
+ thumbnail = None
36
+ yt = self.build_yt(url)
37
+
38
+ if self.is_live(yt.initial_data):
39
+ raise YotubeLiveError("Youtube Live is not supported")
35
40
 
36
- if self.is_live(yt.initial_data):
37
- raise YotubeLiveError("Youtube Live is not supported")
41
+ if yt and yt.thumbnail_url:
42
+ thumbnail = self._download_hndlr(self.download_thumbnail, yt.thumbnail_url)
38
43
 
39
- if yt and yt.thumbnail_url:
40
- thumbnail = self.download_thumbnail(yt.thumbnail_url)
44
+ stream = yt.streams.get_highest_resolution()
41
45
 
42
- stream = yt.streams.get_highest_resolution()
46
+ if not stream:
47
+ raise NotFound("No suitable video stream found")
43
48
 
44
- if not stream:
45
- raise NotFound("No suitable video stream found")
49
+ logging.info("Starting download ...")
46
50
 
47
- local_file = stream.download(
48
- output_path=self.DOWNLOAD_DIR,
49
- max_retries=0,
50
- timeout=timeout,
51
- skip_existing=False,
52
- filename_prefix="yt_download_"
53
- )
54
- logging.debug("Temp filename: '%s'", local_file)
55
- res.append({
56
- "local_media_path": self.rename_local_file(local_file),
57
- "performer": yt.author,
58
- "thumb": thumbnail,
59
- "canonical_name": stream.title,
60
- "media_type": JobType.VIDEO
61
- })
51
+ local_file = stream.download(
52
+ output_path=self.DOWNLOAD_DIR,
53
+ max_retries=0,
54
+ timeout=timeout,
55
+ skip_existing=False,
56
+ filename_prefix="yt_download_"
57
+ )
58
+ logging.debug("Temp filename: '%s'", local_file)
59
+ res.append({
60
+ "local_media_path": self.rename_local_file(local_file),
61
+ "performer": yt.author,
62
+ "thumb": thumbnail,
63
+ "canonical_name": stream.title,
64
+ "media_type": JobType.VIDEO
65
+ })
66
+ except AgeRestrictedError as e:
67
+ raise YotubeAgeRestrictedError("Youtube Age Restricted error")
62
68
 
63
69
  return res
64
70
 
@@ -6,7 +6,7 @@ import asyncio
6
6
 
7
7
  from pyrogram import Client, filters
8
8
  from pyrogram.enums import ParseMode
9
- from pyrogram.handlers import MessageHandler
9
+ from pyrogram.handlers import MessageHandler, CallbackQueryHandler
10
10
  from pyrogram.types import Message, InputMedia, InputMediaAudio, InputMediaPhoto, InputMediaVideo, InputMediaAnimation, InputMediaDocument, InlineKeyboardButton, InlineKeyboardMarkup
11
11
  from pyrogram.errors import RPCError, FloodWait, NetworkMigrate, BadRequest, MultiMediaTooLong, MessageIdInvalid
12
12
 
@@ -64,6 +64,7 @@ class Bot(object):
64
64
 
65
65
  self.uploader = AsyncUploader(
66
66
  storage=self.storage,
67
+ admin_message_callback=self.send_text_to_admin,
67
68
  pool_size=int(os.environ.get("UPLOAD_POOL_SIZE", default=workers_amount)),
68
69
  loop=self.client.loop
69
70
  )
@@ -81,6 +82,7 @@ class Bot(object):
81
82
  self.client.add_handler(MessageHandler(self.handlers.help, filters.command("help")))
82
83
  self.client.add_handler(MessageHandler(self.handlers.random, filters.command("random")))
83
84
  self.client.add_handler(MessageHandler(self.handlers.handler))
85
+ self.client.add_handler(CallbackQueryHandler(self.handlers.simple_button_handler))
84
86
 
85
87
  self.placeholder = PlaceholderMessage(self)
86
88
 
@@ -114,6 +116,30 @@ class Bot(object):
114
116
 
115
117
  return 0
116
118
 
119
+ async def send_text_to_admin(self, text: str) -> int:
120
+ try:
121
+ admin = os.environ.get("TG_BOT_ADMIN_USERNAME", None)
122
+ if not admin:
123
+ raise ValueError("Configuration value `TG_BOT_ADMIN_USERNAME` is empty!")
124
+ message_reply = await self.client.send_message(
125
+ chat_id=admin,
126
+ text=text,
127
+ parse_mode=ParseMode.MARKDOWN,
128
+ reply_markup=InlineKeyboardMarkup(
129
+ [
130
+ [
131
+ InlineKeyboardButton("✅ Done", callback_data="auth_process_done")
132
+ ]
133
+ ]
134
+ )
135
+ )
136
+ return message_reply.id
137
+ except Exception as e:
138
+ logging.error("Failed to send text message to admin!")
139
+ logging.exception(e)
140
+
141
+ return 0
142
+
117
143
  def build_tg_args(self, job: UploadJob) -> dict:
118
144
  args = {}
119
145
  if job.media_type == JobType.VIDEO:
@@ -1,5 +1,5 @@
1
1
  from pyrogram import Client
2
- from pyrogram.types import Message
2
+ from pyrogram.types import Message, CallbackQuery
3
3
  from pyrogram.enums import ChatType, ParseMode
4
4
  from pyrogram.types import Chat, BotCommand
5
5
 
@@ -165,4 +165,13 @@ class Handlers(object):
165
165
  logging.exception(e)
166
166
 
167
167
  if chat.type not in (ChatType.GROUP, ChatType.SUPERGROUP) and not urls:
168
- await self.bot.send_text(rext=reply_text, reply_id=effective_message_id)
168
+ await self.bot.send_text(rext=reply_text, reply_id=effective_message_id)
169
+
170
+ async def simple_button_handler(self, client: Client, query: CallbackQuery) -> None:
171
+ await client.answer_callback_query(
172
+ callback_query_id=query.id,
173
+ text="Please wait, bot will try to download media with obtained credentials.\nIf authorization is not successful, the operation will be repeated.",
174
+ show_alert=True
175
+ )
176
+ self.bot.downloader.auth_event.set()
177
+ self.bot.downloader.auth_event.clear()
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import threading
2
3
  import multiprocessing
3
4
  from warp_beacon.jobs.upload_job import UploadJob
@@ -20,12 +21,14 @@ class AsyncUploader(object):
20
21
  storage = None
21
22
  in_process = set()
22
23
  loop = None
24
+ admin_message_callback = None
23
25
  pool_size = 1
24
26
 
25
- def __init__(self, loop: asyncio.AbstractEventLoop, storage: Storage, pool_size: int=multiprocessing.cpu_count()) -> None:
27
+ def __init__(self, loop: asyncio.AbstractEventLoop, storage: Storage, admin_message_callback: Callable, pool_size: int=min(32, os.cpu_count() + 4)) -> None:
26
28
  self.storage = storage
27
29
  self.loop = loop
28
30
  self.job_queue = multiprocessing.Queue()
31
+ self.admin_message_callback = admin_message_callback
29
32
  self.pool_size = pool_size
30
33
 
31
34
  def __del__(self) -> None:
@@ -80,6 +83,9 @@ class AsyncUploader(object):
80
83
  job = self.job_queue.get()
81
84
  if job is self.__JOE_BIDEN_WAKEUP:
82
85
  continue
86
+ if job.is_message_to_admin and job.message_text and self.admin_message_callback:
87
+ asyncio.ensure_future(self.admin_message_callback(job.message_text), loop=self.loop)
88
+ continue
83
89
  path = ""
84
90
  if job.media_type == JobType.COLLECTION:
85
91
  for i in job.media_collection:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: warp_beacon
3
- Version: 2.0.6
3
+ Version: 2.0.8
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
@@ -1,2 +0,0 @@
1
- __version__ = "2.0.6"
2
-
@@ -1,47 +0,0 @@
1
- from warp_beacon.jobs.types import JobType
2
- from warp_beacon.scraper.youtube.abstract import YoutubeAbstract
3
-
4
- from pytubefix import YouTube
5
-
6
- import logging
7
-
8
- class YoutubeMusicScraper(YoutubeAbstract):
9
- YT_MAX_RETRIES_DEFAULT = 6
10
- YT_PAUSE_BEFORE_RETRY_DEFAULT = 3
11
- YT_TIMEOUT_DEFAULT = 2
12
- YT_TIMEOUT_INCREMENT_DEFAULT = 60
13
-
14
- def _download(self, url: str, timeout: int = 0) -> list:
15
- res = []
16
- thumbnail = None
17
- yt = YouTube(url)
18
- if yt and yt.thumbnail_url:
19
- thumbnail = self.download_thumbnail(yt.thumbnail_url)
20
- stream = yt.streams.get_audio_only()
21
- if stream:
22
- logging.info("Announced audio file size: '%d'", stream.filesize)
23
- if stream.filesize > 2e+9:
24
- logging.warning("Downloading size reported by YouTube is over than 2 GB!")
25
- raise FileTooBig("YouTube file is larger than 2 GB")
26
- logging.info("Operation timeout is '%d'", timeout)
27
- local_file = stream.download(
28
- output_path=self.DOWNLOAD_DIR,
29
- max_retries=0,
30
- timeout=timeout,
31
- skip_existing=False,
32
- filename_prefix='yt_download_',
33
- mp3=True
34
- )
35
- logging.debug("Temp filename: '%s'", local_file)
36
- res.append({
37
- "local_media_path": self.rename_local_file(local_file),
38
- "performer": yt.author,
39
- "thumb": thumbnail,
40
- "canonical_name": stream.title,
41
- "media_type": JobType.AUDIO
42
- })
43
-
44
- return res
45
-
46
- def download(self, url: str) -> list:
47
- return self._download_hndlr(self._download, url)
@@ -1,42 +0,0 @@
1
- from warp_beacon.jobs.types import JobType
2
- from warp_beacon.scraper.youtube.abstract import YoutubeAbstract
3
-
4
- from pytubefix import YouTube
5
-
6
- import logging
7
-
8
- class YoutubeShortsScraper(YoutubeAbstract):
9
- YT_MAX_RETRIES_DEFAULT = 8
10
- YT_PAUSE_BEFORE_RETRY_DEFAULT = 3
11
- YT_TIMEOUT_DEFAULT = 2
12
- YT_TIMEOUT_INCREMENT_DEFAULT = 60
13
-
14
- def _download(self, url: str, timeout: int = 0) -> list:
15
- res = []
16
- thumbnail = None
17
- yt = YouTube(url)
18
- stream = yt.streams.get_highest_resolution()
19
- if yt and yt.thumbnail_url:
20
- logging.debug("Generation thumb for Shorts ...")
21
- thumbnail = self.download_thumbnail(yt.thumbnail_url)
22
- if stream:
23
- local_file = stream.download(
24
- output_path=self.DOWNLOAD_DIR,
25
- max_retries=0,
26
- timeout=timeout,
27
- skip_existing=False,
28
- filename_prefix="yt_download_"
29
- )
30
- logging.debug("Temp filename: '%s'", local_file)
31
- res.append({
32
- "local_media_path": self.rename_local_file(local_file),
33
- "performer": yt.author,
34
- "thumb": thumbnail,
35
- "canonical_name": stream.title,
36
- "media_type": JobType.VIDEO
37
- })
38
-
39
- return res
40
-
41
- def download(self, url: str) -> list:
42
- return self._download_hndlr(self._download, url)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes