warp-beacon 2.0.6__tar.gz → 2.0.7__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.
- {warp_beacon-2.0.6/warp_beacon.egg-info → warp_beacon-2.0.7}/PKG-INFO +1 -1
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/etc/warp_beacon.conf +2 -1
- warp_beacon-2.0.7/warp_beacon/__version__.py +2 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/jobs/abstract.py +4 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/mediainfo/silencer.py +1 -1
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/scraper/__init__.py +24 -4
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/scraper/abstract.py +3 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/scraper/exceptions.py +3 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/scraper/instagram.py +1 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/scraper/youtube/abstract.py +78 -3
- warp_beacon-2.0.7/warp_beacon/scraper/youtube/music.py +53 -0
- warp_beacon-2.0.7/warp_beacon/scraper/youtube/shorts.py +46 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/scraper/youtube/youtube.py +31 -25
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/telegram/bot.py +29 -3
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/telegram/handlers.py +11 -2
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/uploader/__init__.py +7 -1
- {warp_beacon-2.0.6 → warp_beacon-2.0.7/warp_beacon.egg-info}/PKG-INFO +1 -1
- warp_beacon-2.0.6/warp_beacon/__version__.py +0 -2
- warp_beacon-2.0.6/warp_beacon/scraper/youtube/music.py +0 -47
- warp_beacon-2.0.6/warp_beacon/scraper/youtube/shorts.py +0 -42
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/LICENSE +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/MANIFEST.in +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/README.md +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/assets/placeholder.gif +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/etc/.gitignore +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/etc/warp_beacon.service +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/pyproject.toml +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/setup.cfg +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/setup.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/__init__.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/compress/__init__.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/compress/video.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/jobs/__init__.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/jobs/download_job.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/jobs/types.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/jobs/upload_job.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/mediainfo/__init__.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/mediainfo/abstract.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/mediainfo/audio.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/mediainfo/video.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/scraper/youtube/__init__.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/storage/__init__.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/telegram/__init__.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/telegram/placeholder_message.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/telegram/utils.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon/warp_beacon.py +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon.egg-info/SOURCES.txt +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon.egg-info/dependency_links.txt +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon.egg-info/entry_points.txt +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon.egg-info/requires.txt +0 -0
- {warp_beacon-2.0.6 → warp_beacon-2.0.7}/warp_beacon.egg-info/top_level.txt +0 -0
@@ -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
|
@@ -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:
|
@@ -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
|
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(
|
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
|
+
))
|
@@ -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,
|
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
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
41
|
+
if yt and yt.thumbnail_url:
|
42
|
+
thumbnail = self._download_hndlr(self.download_thumbnail, yt.thumbnail_url)
|
38
43
|
|
39
|
-
|
40
|
-
thumbnail = self.download_thumbnail(yt.thumbnail_url)
|
44
|
+
stream = yt.streams.get_highest_resolution()
|
41
45
|
|
42
|
-
|
46
|
+
if not stream:
|
47
|
+
raise NotFound("No suitable video stream found")
|
43
48
|
|
44
|
-
|
45
|
-
raise NotFound("No suitable video stream found")
|
49
|
+
logging.info("Starting download ...")
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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:
|
@@ -342,7 +368,7 @@ class Bot(object):
|
|
342
368
|
except Exception as e:
|
343
369
|
logging.error("Error occurred!")
|
344
370
|
logging.exception(e)
|
345
|
-
finally:
|
346
|
-
job.remove_files()
|
371
|
+
#finally:
|
372
|
+
#job.remove_files()
|
347
373
|
|
348
374
|
return tg_file_ids
|
@@ -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=
|
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,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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|