warp-beacon 2.1.0__tar.gz → 2.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {warp_beacon-2.1.0/warp_beacon.egg-info → warp_beacon-2.1.2}/PKG-INFO +1 -1
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/etc/warp_beacon.conf +2 -2
- warp_beacon-2.1.2/warp_beacon/__version__.py +2 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/jobs/abstract.py +2 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/scraper/__init__.py +47 -9
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/scraper/account_selector.py +3 -2
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/scraper/exceptions.py +6 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/scraper/instagram/instagram.py +11 -18
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/scraper/youtube/abstract.py +9 -8
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/telegram/bot.py +19 -15
- {warp_beacon-2.1.0 → warp_beacon-2.1.2/warp_beacon.egg-info}/PKG-INFO +1 -1
- warp_beacon-2.1.0/warp_beacon/__version__.py +0 -2
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/LICENSE +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/MANIFEST.in +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/README.md +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/assets/placeholder.gif +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/etc/.gitignore +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/etc/accounts.json +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/etc/warp_beacon.service +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/pyproject.toml +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/setup.cfg +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/setup.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/__init__.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/compress/__init__.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/compress/video.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/jobs/__init__.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/jobs/download_job.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/jobs/types.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/jobs/upload_job.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/mediainfo/__init__.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/mediainfo/abstract.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/mediainfo/audio.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/mediainfo/silencer.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/mediainfo/video.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/scraper/abstract.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/scraper/instagram/__init__.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/scraper/youtube/__init__.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/scraper/youtube/music.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/scraper/youtube/shorts.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/scraper/youtube/youtube.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/storage/__init__.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/telegram/__init__.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/telegram/handlers.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/telegram/placeholder_message.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/telegram/utils.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/uploader/__init__.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon/warp_beacon.py +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon.egg-info/SOURCES.txt +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon.egg-info/dependency_links.txt +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon.egg-info/entry_points.txt +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon.egg-info/requires.txt +0 -0
- {warp_beacon-2.1.0 → warp_beacon-2.1.2}/warp_beacon.egg-info/top_level.txt +0 -0
@@ -1,11 +1,11 @@
|
|
1
1
|
TG_TOKEN=""
|
2
2
|
TG_BOT_NAME=""
|
3
|
-
|
3
|
+
TG_BOT_ADMINS_USERNAMES=""
|
4
4
|
TG_API_ID=""
|
5
5
|
TG_API_HASH=""
|
6
6
|
TG_BOT_NAME=""
|
7
|
-
TG_BOT_ADMIN_USERNAME=""
|
8
7
|
IG_MAX_RETRIES=10
|
8
|
+
IG_REQUEST_TIMEOUT=60
|
9
9
|
MONGODB_HOST="mongodb"
|
10
10
|
MONGODB_PORT="27017"
|
11
11
|
MONGODB_USER="root"
|
@@ -34,6 +34,7 @@ class JobSettings(TypedDict):
|
|
34
34
|
source_username: str
|
35
35
|
unvailable_error_count: int
|
36
36
|
geoblock_error_count: int
|
37
|
+
account_switches: int
|
37
38
|
|
38
39
|
class AbstractJob(ABC):
|
39
40
|
job_id: uuid.UUID = None
|
@@ -62,6 +63,7 @@ class AbstractJob(ABC):
|
|
62
63
|
source_usename: str = ""
|
63
64
|
unvailable_error_count: int = 0
|
64
65
|
geoblock_error_count: int = 0
|
66
|
+
account_switches: int = 0
|
65
67
|
|
66
68
|
def __init__(self, **kwargs: Unpack[JobSettings]) -> None:
|
67
69
|
if kwargs:
|
@@ -4,7 +4,7 @@ from typing import Optional
|
|
4
4
|
import multiprocessing
|
5
5
|
from queue import Empty
|
6
6
|
|
7
|
-
from warp_beacon.scraper.exceptions import NotFound, UnknownError, TimeOut, Unavailable, FileTooBig, YotubeLiveError, YotubeAgeRestrictedError, IGRateLimitAccured
|
7
|
+
from warp_beacon.scraper.exceptions import NotFound, UnknownError, TimeOut, Unavailable, FileTooBig, YotubeLiveError, YotubeAgeRestrictedError, IGRateLimitAccured, CaptchaIssue, AllAccountsFailed
|
8
8
|
from warp_beacon.mediainfo.video import VideoInfo
|
9
9
|
from warp_beacon.mediainfo.audio import AudioInfo
|
10
10
|
from warp_beacon.mediainfo.silencer import Silencer
|
@@ -65,6 +65,17 @@ class AsyncDownloader(object):
|
|
65
65
|
|
66
66
|
return media_info
|
67
67
|
|
68
|
+
def try_next_account(self, job: DownloadJob, report_error: str = None) -> None:
|
69
|
+
logging.warning("Switching account!")
|
70
|
+
if job.account_switches > self.acc_selector.count_service_accounts(job.job_origin):
|
71
|
+
raise AllAccountsFailed()
|
72
|
+
if report_error:
|
73
|
+
self.acc_selector.bump_acc_fail("rate_limits")
|
74
|
+
self.acc_selector.next()
|
75
|
+
cur_acc = self.acc_selector.get_current()
|
76
|
+
logging.info("Current account: '%s'", str(cur_acc))
|
77
|
+
job.account_switches += 1
|
78
|
+
|
68
79
|
def do_work(self) -> None:
|
69
80
|
logging.info("download worker started")
|
70
81
|
while self.allow_loop.value == 1:
|
@@ -108,11 +119,14 @@ class AsyncDownloader(object):
|
|
108
119
|
job_failed=True,
|
109
120
|
job_failed_msg="Unable to access to media under this URL. Seems like the media is private.")
|
110
121
|
)
|
122
|
+
self.send_message_to_admin(
|
123
|
+
f"Task {job.job_id} failed. URL: '{job.url}'. Reason: 'NotFound'."
|
124
|
+
)
|
111
125
|
break
|
112
126
|
except Unavailable as e:
|
113
127
|
logging.warning("Not found or unavailable error occurred!")
|
114
128
|
logging.exception(e)
|
115
|
-
if job.unvailable_error_count > self.acc_selector.count_service_accounts(job.job_origin
|
129
|
+
if job.unvailable_error_count > self.acc_selector.count_service_accounts(job.job_origin):
|
116
130
|
self.uploader.queue_task(job.to_upload_job(
|
117
131
|
job_failed=True,
|
118
132
|
job_failed_msg="Video is unvailable for all your service accounts.")
|
@@ -128,7 +142,10 @@ class AsyncDownloader(object):
|
|
128
142
|
logging.exception(e)
|
129
143
|
self.uploader.queue_task(job.to_upload_job(
|
130
144
|
job_failed=True,
|
131
|
-
job_failed_msg="Failed to download content. Please check you Internet connection
|
145
|
+
job_failed_msg="Failed to download content due timeout error. Please check you Internet connection, retry amount or request timeout bot configuration settings.")
|
146
|
+
)
|
147
|
+
self.send_message_to_admin(
|
148
|
+
f"Task {job.job_id} failed. URL: '{job.url}'. Reason: 'TimeOut'."
|
132
149
|
)
|
133
150
|
break
|
134
151
|
except FileTooBig as e:
|
@@ -138,15 +155,22 @@ class AsyncDownloader(object):
|
|
138
155
|
job_failed=True,
|
139
156
|
job_failed_msg="Unfortunately this file has exceeded the Telegram limits. A file cannot be larger than 2 gigabytes.")
|
140
157
|
)
|
158
|
+
self.send_message_to_admin(
|
159
|
+
f"Task {job.job_id} failed. URL: '{job.url}'. Reason: 'FileTooBig'."
|
160
|
+
)
|
141
161
|
break
|
142
162
|
except IGRateLimitAccured as e:
|
143
163
|
logging.warning("IG ratelimit accured :(")
|
144
164
|
logging.exception(e)
|
145
|
-
|
146
|
-
self.
|
147
|
-
|
148
|
-
|
149
|
-
logging.
|
165
|
+
self.try_next_account(job, report_error="rate_limits")
|
166
|
+
self.job_queue.put(job)
|
167
|
+
break
|
168
|
+
except CaptchaIssue as e:
|
169
|
+
logging.warning("Challange accured!")
|
170
|
+
logging.exception(e)
|
171
|
+
self.try_next_account(job)
|
172
|
+
self.job_queue.put(job)
|
173
|
+
break
|
150
174
|
except YotubeLiveError as e:
|
151
175
|
logging.warning("Youtube Live videos are not supported. Skipping.")
|
152
176
|
logging.exception(e)
|
@@ -162,6 +186,20 @@ class AsyncDownloader(object):
|
|
162
186
|
job_failed=True,
|
163
187
|
job_failed_msg="Youtube Age Restricted error. Check your bot Youtube account settings.")
|
164
188
|
)
|
189
|
+
self.send_message_to_admin(
|
190
|
+
f"Task {job.job_id} failed. URL: '{job.url}'. Reason: 'YotubeAgeRestrictedError'."
|
191
|
+
)
|
192
|
+
break
|
193
|
+
except AllAccountsFailed as e:
|
194
|
+
logging.error("All accounts failed!")
|
195
|
+
logging.exception(e)
|
196
|
+
self.uploader.queue_task(job.to_upload_job(
|
197
|
+
job_failed=True,
|
198
|
+
job_failed_msg="All bot accounts failed to download content. Bot administrator noticed about the issue.")
|
199
|
+
)
|
200
|
+
self.send_message_to_admin(
|
201
|
+
f"Task {job.job_id} failed. URL: '{job.url}'. Reason: 'AllAccountsFailed'."
|
202
|
+
)
|
165
203
|
break
|
166
204
|
except (UnknownError, Exception) as e:
|
167
205
|
logging.warning("UnknownError occurred!")
|
@@ -172,7 +210,7 @@ class AsyncDownloader(object):
|
|
172
210
|
else:
|
173
211
|
exception_msg = str(e)
|
174
212
|
if "geoblock_required" in exception_msg:
|
175
|
-
if job.geoblock_error_count > self.acc_selector.count_service_accounts(job.job_origin
|
213
|
+
if job.geoblock_error_count > self.acc_selector.count_service_accounts(job.job_origin):
|
176
214
|
self.uploader.queue_task(job.to_upload_job(
|
177
215
|
job_failed=True,
|
178
216
|
job_failed_msg="This content does not accessible for all yout bot accounts. Seems like author blocked certain regions.")
|
@@ -77,5 +77,6 @@ class AccountSelector(object):
|
|
77
77
|
def get_meta_data(self) -> dict:
|
78
78
|
return self.accounts_meta_data[self.current_module_name][self.index]
|
79
79
|
|
80
|
-
def count_service_accounts(self, mod_name:
|
81
|
-
|
80
|
+
def count_service_accounts(self, mod_name: Origin) -> int:
|
81
|
+
module_name = 'youtube' if next((s for s in ("yt", "youtube", "youtu_be") if s in mod_name.value), None) else 'instagram'
|
82
|
+
return len(self.accounts_meta_data[module_name])
|
@@ -17,9 +17,9 @@ from instagrapi.mixins.story import Story
|
|
17
17
|
#from instagrapi.types import Media
|
18
18
|
from instagrapi import Client
|
19
19
|
from instagrapi.mixins.challenge import ChallengeChoice
|
20
|
-
from instagrapi.exceptions import LoginRequired, PleaseWaitFewMinutes, MediaNotFound, ClientNotFoundError, UserNotFound, UnknownError as IGUnknownError
|
20
|
+
from instagrapi.exceptions import LoginRequired, PleaseWaitFewMinutes, MediaNotFound, ClientNotFoundError, UserNotFound, ChallengeRequired, ChallengeSelfieCaptcha, UnknownError as IGUnknownError
|
21
21
|
|
22
|
-
from warp_beacon.scraper.exceptions import NotFound, UnknownError, TimeOut, IGRateLimitAccured, extract_exception_message
|
22
|
+
from warp_beacon.scraper.exceptions import NotFound, UnknownError, TimeOut, IGRateLimitAccured, CaptchaIssue, extract_exception_message
|
23
23
|
from warp_beacon.scraper.abstract import ScraperAbstract
|
24
24
|
from warp_beacon.jobs.types import JobType
|
25
25
|
from warp_beacon.telegram.utils import Utils
|
@@ -37,6 +37,7 @@ class InstagramScraper(ScraperAbstract):
|
|
37
37
|
#
|
38
38
|
self.inst_session_file = INST_SESSION_FILE_TPL % self.account_index
|
39
39
|
self.cl = Client()
|
40
|
+
self.cl.request_timeout = int(os.environ.get("IG_REQUEST_TIMEOUT", default=60))
|
40
41
|
self.setup_device()
|
41
42
|
self.cl.challenge_code_handler = self.challenge_code_handler
|
42
43
|
self.cl.change_password_handler = self.change_password_handler
|
@@ -85,19 +86,7 @@ class InstagramScraper(ScraperAbstract):
|
|
85
86
|
|
86
87
|
def scrap(self, url: str) -> tuple[str]:
|
87
88
|
self.load_session()
|
88
|
-
|
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)
|
89
|
+
self._download_hndlr(self.cl.get_timeline_feed)
|
101
90
|
def _scrap() -> tuple[str]:
|
102
91
|
if "stories" in url:
|
103
92
|
# remove URL options
|
@@ -141,6 +130,10 @@ class InstagramScraper(ScraperAbstract):
|
|
141
130
|
try:
|
142
131
|
ret_val = func(*args, **kwargs)
|
143
132
|
break
|
133
|
+
except (ChallengeRequired, ChallengeSelfieCaptcha) as e:
|
134
|
+
logging.warning("Instagram wants Challange!")
|
135
|
+
logging.exception(e)
|
136
|
+
raise CaptchaIssue()
|
144
137
|
except LoginRequired as e:
|
145
138
|
logging.error("LoginRequired occurred in download handler!")
|
146
139
|
logging.exception(e)
|
@@ -236,7 +229,7 @@ class InstagramScraper(ScraperAbstract):
|
|
236
229
|
res = []
|
237
230
|
wait_timeout = int(os.environ.get("IG_WAIT_TIMEOUT", default=60))
|
238
231
|
timeout_increment = int(os.environ.get("IG_TIMEOUT_INCREMENT", default=30))
|
239
|
-
ratelimit_threshold = int(os.environ.get("IG_RATELIMIT_TRESHOLD", default=
|
232
|
+
ratelimit_threshold = int(os.environ.get("IG_RATELIMIT_TRESHOLD", default=3))
|
240
233
|
please_wait_few_minutes_count = 1
|
241
234
|
while True:
|
242
235
|
try:
|
@@ -288,8 +281,8 @@ class InstagramScraper(ScraperAbstract):
|
|
288
281
|
|
289
282
|
def email_challenge_resolver(self, username: str) -> Optional[str]:
|
290
283
|
logging.info("Started email challenge resolver")
|
291
|
-
mail = imaplib.IMAP4_SSL(
|
292
|
-
mail.login(
|
284
|
+
mail = imaplib.IMAP4_SSL(self.account.get("imap_server", default="imap.bagrintsev.me"))
|
285
|
+
mail.login(self.account.get("imap_login", default=""), self.account.get("imap_password", default="")) # email server creds
|
293
286
|
mail.select("inbox")
|
294
287
|
_, data = mail.search(None, "(UNSEEN)")
|
295
288
|
ids = data.pop().split()
|
@@ -17,7 +17,7 @@ from PIL import Image
|
|
17
17
|
|
18
18
|
from warp_beacon.scraper.abstract import ScraperAbstract
|
19
19
|
from warp_beacon.mediainfo.abstract import MediaInfoAbstract
|
20
|
-
from warp_beacon.scraper.exceptions import
|
20
|
+
from warp_beacon.scraper.exceptions import TimeOut, Unavailable, extract_exception_message
|
21
21
|
|
22
22
|
from pytubefix import YouTube
|
23
23
|
from pytubefix.innertube import _default_clients
|
@@ -52,7 +52,8 @@ def patched_fetch_bearer_token(self) -> None:
|
|
52
52
|
self.send_message_to_admin_func(
|
53
53
|
f"Please open {verification_url} and input code `{user_code}`.\n\n"
|
54
54
|
"Please select a Google account with verified age.\n"
|
55
|
-
"This will allow you to avoid error the **AgeRestrictedError** when accessing some content."
|
55
|
+
"This will allow you to avoid error the **AgeRestrictedError** when accessing some content.",
|
56
|
+
yt_auth=True)
|
56
57
|
self.auth_event.wait()
|
57
58
|
|
58
59
|
data = {
|
@@ -137,12 +138,12 @@ class YoutubeAbstract(ScraperAbstract):
|
|
137
138
|
# do noting, not interested
|
138
139
|
pass
|
139
140
|
#except http.client.IncompleteRead as e:
|
140
|
-
except (socket.timeout,
|
141
|
-
ssl.SSLError,
|
142
|
-
http.client.IncompleteRead,
|
143
|
-
http.client.HTTPException,
|
144
|
-
requests.RequestException,
|
145
|
-
urllib.error.URLError,
|
141
|
+
except (socket.timeout,
|
142
|
+
ssl.SSLError,
|
143
|
+
http.client.IncompleteRead,
|
144
|
+
http.client.HTTPException,
|
145
|
+
requests.RequestException,
|
146
|
+
urllib.error.URLError,
|
146
147
|
urllib.error.HTTPError) as e:
|
147
148
|
if hasattr(e, "code") and int(e.code) == 403:
|
148
149
|
raise Unavailable(extract_exception_message(e))
|
@@ -116,29 +116,33 @@ class Bot(object):
|
|
116
116
|
|
117
117
|
return 0
|
118
118
|
|
119
|
-
async def send_text_to_admin(self, text: str) -> int:
|
119
|
+
async def send_text_to_admin(self, text: str, yt_auth: bool = False) -> list[int]:
|
120
120
|
try:
|
121
|
-
|
122
|
-
if not
|
121
|
+
admins = os.environ.get("TG_BOT_ADMINS_USERNAMES", None)
|
122
|
+
if not admins:
|
123
123
|
raise ValueError("Configuration value `TG_BOT_ADMIN_USERNAME` is empty!")
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
124
|
+
|
125
|
+
msg_ids = []
|
126
|
+
admins_array = admins.split(',')
|
127
|
+
for adm in admins_array:
|
128
|
+
adm = adm.strip()
|
129
|
+
msg_opts = {"chat_id": adm, "text": text, "parse_mode": ParseMode.MARKDOWN}
|
130
|
+
if yt_auth:
|
131
|
+
msg_opts["reply_markup"] = InlineKeyboardMarkup(
|
130
132
|
[
|
131
|
-
|
133
|
+
[
|
134
|
+
InlineKeyboardButton("✅ Done", callback_data="auth_process_done")
|
135
|
+
]
|
132
136
|
]
|
133
|
-
|
134
|
-
)
|
135
|
-
|
136
|
-
return
|
137
|
+
)
|
138
|
+
message_reply = await self.client.send_message(**msg_opts)
|
139
|
+
msg_ids.append(message_reply.id)
|
140
|
+
return msg_ids
|
137
141
|
except Exception as e:
|
138
142
|
logging.error("Failed to send text message to admin!")
|
139
143
|
logging.exception(e)
|
140
144
|
|
141
|
-
return
|
145
|
+
return []
|
142
146
|
|
143
147
|
def build_tg_args(self, job: UploadJob) -> dict:
|
144
148
|
args = {}
|
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
|
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
|