Open-AutoTools 0.0.3rc5__py3-none-any.whl → 0.0.4rc1__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.
- autotools/autocaps/commands.py +3 -7
- autotools/autocaps/core.py +5 -4
- autotools/autoip/commands.py +6 -11
- autotools/autoip/core.py +151 -200
- autotools/autolower/commands.py +3 -7
- autotools/autolower/core.py +4 -3
- autotools/autopassword/commands.py +27 -33
- autotools/autopassword/core.py +32 -73
- autotools/autotest/__init__.py +2 -0
- autotools/autotest/commands.py +205 -0
- autotools/cli.py +123 -62
- autotools/utils/commands.py +13 -0
- autotools/utils/loading.py +14 -6
- autotools/utils/performance.py +392 -0
- autotools/utils/updates.py +30 -22
- autotools/utils/version.py +69 -63
- open_autotools-0.0.4rc1.dist-info/METADATA +103 -0
- open_autotools-0.0.4rc1.dist-info/RECORD +28 -0
- {Open_AutoTools-0.0.3rc5.dist-info → open_autotools-0.0.4rc1.dist-info}/WHEEL +1 -1
- {Open_AutoTools-0.0.3rc5.dist-info → open_autotools-0.0.4rc1.dist-info}/entry_points.txt +0 -3
- Open_AutoTools-0.0.3rc5.dist-info/METADATA +0 -317
- Open_AutoTools-0.0.3rc5.dist-info/RECORD +0 -44
- autotools/autocaps/tests/__init__.py +0 -1
- autotools/autocaps/tests/test_autocaps_core.py +0 -45
- autotools/autocaps/tests/test_autocaps_integration.py +0 -46
- autotools/autodownload/__init__.py +0 -0
- autotools/autodownload/commands.py +0 -38
- autotools/autodownload/core.py +0 -433
- autotools/autoip/tests/__init__.py +0 -1
- autotools/autoip/tests/test_autoip_core.py +0 -72
- autotools/autoip/tests/test_autoip_integration.py +0 -92
- autotools/autolower/tests/__init__.py +0 -1
- autotools/autolower/tests/test_autolower_core.py +0 -45
- autotools/autolower/tests/test_autolower_integration.py +0 -46
- autotools/autospell/__init__.py +0 -3
- autotools/autospell/commands.py +0 -123
- autotools/autospell/core.py +0 -222
- autotools/autotranslate/__init__.py +0 -3
- autotools/autotranslate/commands.py +0 -42
- autotools/autotranslate/core.py +0 -52
- autotools/test/__init__.py +0 -3
- autotools/test/commands.py +0 -118
- {Open_AutoTools-0.0.3rc5.dist-info → open_autotools-0.0.4rc1.dist-info/licenses}/LICENSE +0 -0
- {Open_AutoTools-0.0.3rc5.dist-info → open_autotools-0.0.4rc1.dist-info}/top_level.txt +0 -0
autotools/autodownload/core.py
DELETED
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
import requests
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from urllib.parse import urlsplit
|
|
5
|
-
from tqdm import tqdm
|
|
6
|
-
import yt_dlp
|
|
7
|
-
import platform
|
|
8
|
-
import subprocess
|
|
9
|
-
import json
|
|
10
|
-
from rich.progress import Progress
|
|
11
|
-
from ..utils.loading import LoadingAnimation
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# FUNCTION TO GET DEFAULT DOWNLOAD DIRECTORY
|
|
15
|
-
def get_default_download_dir():
|
|
16
|
-
return Path(os.getenv('USERPROFILE') if os.name == 'nt' else Path.home()) / 'Downloads'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# FUNCTION TO GET FILENAME FROM URL WITH DEFAULT AND EXTENSION HANDLING
|
|
20
|
-
def get_filename_from_url(url):
|
|
21
|
-
filename = os.path.basename(urlsplit(url).path)
|
|
22
|
-
if not filename: # IF NO FILENAME IN URL
|
|
23
|
-
return "downloaded_file"
|
|
24
|
-
if not Path(filename).suffix: # IF NO EXTENSION IN FILENAME
|
|
25
|
-
return f"{filename}.bin"
|
|
26
|
-
return filename
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# FUNCTION TO OPEN DOWNLOAD DIRECTORY AFTER DOWNLOAD IS COMPLETE
|
|
30
|
-
def open_download_folder(path):
|
|
31
|
-
"""OPEN THE DOWNLOAD FOLDER IN THE DEFAULT FILE MANAGER"""
|
|
32
|
-
# SKIP IN CI ENVIRONMENT
|
|
33
|
-
if os.environ.get('CI'):
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
if platform.system() == 'Darwin': # MACOS
|
|
38
|
-
subprocess.run(['open', str(path)], check=True)
|
|
39
|
-
elif platform.system() == 'Windows': # WINDOWS
|
|
40
|
-
os.startfile(str(path))
|
|
41
|
-
else: # LINUX
|
|
42
|
-
subprocess.run(['xdg-open', str(path)], check=True)
|
|
43
|
-
except Exception as e:
|
|
44
|
-
print(f"Failed to open download folder: {e}")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# FUNCTION TO VALIDATE YOUTUBE URL FORMAT
|
|
48
|
-
def validate_youtube_url(url):
|
|
49
|
-
"""BASIC URL VALIDATION WITH PROPER FORMAT CHECK"""
|
|
50
|
-
# CHECK IF URL CONTAINS YOUTUBE DOMAIN
|
|
51
|
-
is_youtube = any(domain in url for domain in ["youtube.com", "youtu.be", "music.youtube.com"])
|
|
52
|
-
|
|
53
|
-
# CHECK IF URL HAS PROPER VIDEO ID FORMAT
|
|
54
|
-
has_video_id = False
|
|
55
|
-
if "youtube.com/watch" in url and "v=" in url:
|
|
56
|
-
has_video_id = True
|
|
57
|
-
elif "youtu.be/" in url and len(url.split("youtu.be/")[1]) > 0:
|
|
58
|
-
has_video_id = True
|
|
59
|
-
elif any(pattern in url for pattern in ["/watch/", "/shorts/", "/live/"]):
|
|
60
|
-
path_parts = url.split("/")
|
|
61
|
-
has_video_id = len(path_parts[-1]) > 0
|
|
62
|
-
elif "attribution_link" in url and "watch?v=" in url:
|
|
63
|
-
has_video_id = True
|
|
64
|
-
|
|
65
|
-
return is_youtube and has_video_id
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# FUNCTION TO DOWNLOAD FILES WITH REQUESTS, INCLUDING ERROR HANDLING AND PROGRESS BAR
|
|
69
|
-
def download_file(url):
|
|
70
|
-
download_dir = get_default_download_dir()
|
|
71
|
-
filename = get_filename_from_url(url)
|
|
72
|
-
dest_file = download_dir / filename
|
|
73
|
-
|
|
74
|
-
try:
|
|
75
|
-
with requests.get(url, stream=True) as response:
|
|
76
|
-
response.raise_for_status()
|
|
77
|
-
|
|
78
|
-
total_size = int(response.headers.get('content-length', 0))
|
|
79
|
-
block_size = 1024 # 1KB
|
|
80
|
-
|
|
81
|
-
with tqdm(total=total_size if total_size else None, unit='iB', unit_scale=True, desc=f"Downloading {filename}", leave=True) as tqdm_bar:
|
|
82
|
-
with open(dest_file, 'wb') as file:
|
|
83
|
-
for chunk in response.iter_content(chunk_size=block_size):
|
|
84
|
-
if chunk:
|
|
85
|
-
file.write(chunk)
|
|
86
|
-
tqdm_bar.update(len(chunk))
|
|
87
|
-
|
|
88
|
-
# AUTOMATICALLY OPEN DOWNLOAD FOLDER AFTER FILE DOWNLOAD IS COMPLETE
|
|
89
|
-
open_download_folder(download_dir)
|
|
90
|
-
except requests.exceptions.RequestException as e:
|
|
91
|
-
print(f"Error during file download: {e}")
|
|
92
|
-
|
|
93
|
-
# FUNCTION TO GET CONSENT FILE PATH
|
|
94
|
-
def get_consent_file_path():
|
|
95
|
-
"""GET PATH TO STORE CONSENT STATUS"""
|
|
96
|
-
# INFO: delete consent file with "rm -f ~/.autotools/consent.json" if you want to force new consent in local development
|
|
97
|
-
return Path.home() / '.autotools' / 'consent.json'
|
|
98
|
-
|
|
99
|
-
# FUNCTION TO LOAD CONSENT STATUS
|
|
100
|
-
def load_consent_status():
|
|
101
|
-
"""LOAD SAVED CONSENT STATUS"""
|
|
102
|
-
try:
|
|
103
|
-
consent_file = get_consent_file_path()
|
|
104
|
-
|
|
105
|
-
# FORCE NEW CONSENT IF FILE DOESN'T EXIST OR IS EMPTY
|
|
106
|
-
if not consent_file.exists():
|
|
107
|
-
return False
|
|
108
|
-
|
|
109
|
-
# READ CONSENT STATUS
|
|
110
|
-
with open(consent_file) as f:
|
|
111
|
-
data = json.load(f)
|
|
112
|
-
return data.get('youtube_consent', False)
|
|
113
|
-
except Exception:
|
|
114
|
-
# IF ANY ERROR OCCURS, FORCE NEW CONSENT
|
|
115
|
-
return False
|
|
116
|
-
|
|
117
|
-
# FUNCTION TO SAVE CONSENT STATUS
|
|
118
|
-
def save_consent_status(status):
|
|
119
|
-
"""SAVE CONSENT STATUS"""
|
|
120
|
-
try:
|
|
121
|
-
consent_file = get_consent_file_path()
|
|
122
|
-
consent_file.parent.mkdir(exist_ok=True)
|
|
123
|
-
|
|
124
|
-
# SAVE CONSENT STATUS TO FILE
|
|
125
|
-
with open(consent_file, 'w') as f:
|
|
126
|
-
json.dump({'youtube_consent': status}, f)
|
|
127
|
-
return True
|
|
128
|
-
except Exception:
|
|
129
|
-
# IF SAVING FAILS, RETURN FALSE TO FORCE NEW CONSENT NEXT TIME
|
|
130
|
-
return False
|
|
131
|
-
|
|
132
|
-
# FUNCTION TO SAFELY PRINT WITH EMOJI FALLBACK
|
|
133
|
-
def safe_print(text):
|
|
134
|
-
"""PRINT TEXT WITH EMOJI FALLBACK FOR WINDOWS"""
|
|
135
|
-
try:
|
|
136
|
-
print(text)
|
|
137
|
-
except UnicodeEncodeError:
|
|
138
|
-
# REPLACE EMOJIS WITH ASCII ALTERNATIVES
|
|
139
|
-
text = (text.replace('⚠️', '!')
|
|
140
|
-
.replace('🔍', '*')
|
|
141
|
-
.replace('🎥', '>')
|
|
142
|
-
.replace('📋', '+')
|
|
143
|
-
.replace('❌', 'X')
|
|
144
|
-
.replace('✅', 'V')
|
|
145
|
-
.replace('↓', 'v'))
|
|
146
|
-
print(text)
|
|
147
|
-
|
|
148
|
-
# FUNCTION TO GET USER CONSENT WITH INTERACTIVE PROMPT
|
|
149
|
-
def get_user_consent():
|
|
150
|
-
"""GET USER CONSENT WITH INTERACTIVE PROMPT"""
|
|
151
|
-
safe_print("\n! Important Notice:")
|
|
152
|
-
print("This tool will:")
|
|
153
|
-
print("1. Download video content from YouTube")
|
|
154
|
-
print("2. Save files to your local machine")
|
|
155
|
-
print("3. Use mobile API for better compatibility")
|
|
156
|
-
|
|
157
|
-
# GET USER CONSENT WITH INTERACTIVE PROMPT
|
|
158
|
-
while True:
|
|
159
|
-
response = input("\nDo you consent to these actions? (yes/no): ").lower()
|
|
160
|
-
if response in ['yes', 'y']:
|
|
161
|
-
save_consent_status(True)
|
|
162
|
-
return True
|
|
163
|
-
elif response in ['no', 'n']:
|
|
164
|
-
save_consent_status(False)
|
|
165
|
-
return False
|
|
166
|
-
print("Please answer 'yes' or 'no'")
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
# FUNCTION TO CHECK IF VIDEO EXISTS AND GET USER CONSENT FOR REPLACEMENT
|
|
170
|
-
def check_existing_video(info, format='mp4'):
|
|
171
|
-
"""CHECK IF VIDEO EXISTS AND ASK FOR REPLACEMENT"""
|
|
172
|
-
download_dir = get_default_download_dir()
|
|
173
|
-
title = info.get('title', '').replace('/', '_') # SANITIZE TITLE
|
|
174
|
-
filename = f"{title}.{format}"
|
|
175
|
-
filepath = download_dir / filename
|
|
176
|
-
|
|
177
|
-
# CHECK IF FILE EXISTS AND ASK FOR REPLACEMENT
|
|
178
|
-
if filepath.exists():
|
|
179
|
-
print(f"\n⚠️ File already exists: {filename}")
|
|
180
|
-
while True:
|
|
181
|
-
response = input("Do you want to replace it? (yes/no): ").lower()
|
|
182
|
-
if response in ['yes', 'y']:
|
|
183
|
-
return True
|
|
184
|
-
elif response in ['no', 'n']:
|
|
185
|
-
# OPEN DOWNLOADS FOLDER TO SHOW EXISTING FILE
|
|
186
|
-
open_download_folder(download_dir)
|
|
187
|
-
return False
|
|
188
|
-
print("Please answer 'yes' or 'no'")
|
|
189
|
-
return True
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
# FUNCTION TO DOWNLOAD YOUTUBE VIDEOS WITH YT-DLP AND SPECIFIED FORMAT AND QUALITY
|
|
193
|
-
def download_youtube_video(url, format='mp4', quality='best'):
|
|
194
|
-
"""DOWNLOAD VIDEO WITH CONSENT CHECK"""
|
|
195
|
-
# VALIDATE URL FIRST
|
|
196
|
-
if not validate_youtube_url(url):
|
|
197
|
-
safe_print("\nX Invalid YouTube URL")
|
|
198
|
-
return False
|
|
199
|
-
|
|
200
|
-
# CHECK FOR SAVED CONSENT FIRST AND GET NEW CONSENT IF NEEDED
|
|
201
|
-
if not load_consent_status() and not get_user_consent():
|
|
202
|
-
safe_print("\nX Download cancelled by user")
|
|
203
|
-
return False
|
|
204
|
-
|
|
205
|
-
# FIRST CHECK VIDEO INFO AND EXISTENCE
|
|
206
|
-
try:
|
|
207
|
-
with yt_dlp.YoutubeDL({
|
|
208
|
-
'quiet': True,
|
|
209
|
-
'no_warnings': True,
|
|
210
|
-
'extractor_args': {'youtube': {
|
|
211
|
-
'player_client': ['web', 'android'],
|
|
212
|
-
'formats': ['missing_pot'] # ALLOW FORMATS WITHOUT PO TOKEN
|
|
213
|
-
}}
|
|
214
|
-
}) as ydl:
|
|
215
|
-
info = ydl.extract_info(url, download=False)
|
|
216
|
-
formats = info.get('formats', [])
|
|
217
|
-
if not formats:
|
|
218
|
-
safe_print("\nX No formats available for this video")
|
|
219
|
-
return False
|
|
220
|
-
|
|
221
|
-
# FIND BEST AVAILABLE QUALITY
|
|
222
|
-
best_height = 0
|
|
223
|
-
for f in formats:
|
|
224
|
-
height = f.get('height')
|
|
225
|
-
if height is not None and height > best_height:
|
|
226
|
-
best_height = height
|
|
227
|
-
|
|
228
|
-
# IF NO VALID HEIGHT FOUND, DEFAULT TO 1080P
|
|
229
|
-
if best_height == 0:
|
|
230
|
-
best_height = 1080
|
|
231
|
-
|
|
232
|
-
# IF QUALITY IS 'BEST', USE THE BEST AVAILABLE
|
|
233
|
-
if quality == 'best':
|
|
234
|
-
height = best_height
|
|
235
|
-
# ASK FOR CONFIRMATION IF 4K OR HIGHER (ONLY FOR MP4)
|
|
236
|
-
if format == 'mp4' and height >= 2160:
|
|
237
|
-
safe_print(f"\n! This video is available in {height}p quality!")
|
|
238
|
-
while True:
|
|
239
|
-
response = input(f"Do you want to download in {height}p quality? (yes/no): ").lower()
|
|
240
|
-
if response in ['no', 'n']:
|
|
241
|
-
height = 1080
|
|
242
|
-
print("\nDowngrading to 1080p quality.")
|
|
243
|
-
break
|
|
244
|
-
elif response in ['yes', 'y']:
|
|
245
|
-
break
|
|
246
|
-
print("Please answer 'yes' or 'no'")
|
|
247
|
-
else:
|
|
248
|
-
# EXTRACT HEIGHT FROM QUALITY STRING
|
|
249
|
-
try:
|
|
250
|
-
height = int(quality.lower().replace('p', ''))
|
|
251
|
-
except ValueError:
|
|
252
|
-
height = 1080 # DEFAULT TO 1080P IF INVALID FORMAT
|
|
253
|
-
|
|
254
|
-
# CHECK IF FILE EXISTS AND GET REPLACEMENT CONSENT
|
|
255
|
-
force_download = check_existing_video(info, format)
|
|
256
|
-
if not force_download:
|
|
257
|
-
safe_print("\nX Download cancelled - file already exists")
|
|
258
|
-
return False
|
|
259
|
-
|
|
260
|
-
# OPEN DOWNLOADS FOLDER IF STARTING NEW DOWNLOAD OR REPLACING
|
|
261
|
-
download_dir = get_default_download_dir()
|
|
262
|
-
open_download_folder(download_dir)
|
|
263
|
-
|
|
264
|
-
except Exception as e:
|
|
265
|
-
safe_print(f"\nX Error checking video: {str(e)}")
|
|
266
|
-
return False
|
|
267
|
-
|
|
268
|
-
loading = LoadingAnimation()
|
|
269
|
-
|
|
270
|
-
# START LOADING FOR DOWNLOAD PROCESS
|
|
271
|
-
with loading:
|
|
272
|
-
loading._spinner.start()
|
|
273
|
-
safe_print("\n* Starting download...")
|
|
274
|
-
|
|
275
|
-
safe_print(f"\n> Downloading video from: {url}")
|
|
276
|
-
if format == 'mp3':
|
|
277
|
-
safe_print(f"+ Format: {format}\n")
|
|
278
|
-
else:
|
|
279
|
-
safe_print(f"+ Format: {format}, Quality: {height}p\n")
|
|
280
|
-
|
|
281
|
-
# YT-DLP PERMISSION OPTIONS FOR DOWNLOADING YOUTUBE VIDEOS
|
|
282
|
-
ydl_opts = {
|
|
283
|
-
'format': (
|
|
284
|
-
f'bestvideo[height<={height}][ext=mp4]+bestaudio[ext=m4a]/' # TRY EXACT HEIGHT MATCH FIRST
|
|
285
|
-
f'bestvideo[height<={height}][ext=webm]+bestaudio[ext=webm]/' # TRY WEBM AS FALLBACK
|
|
286
|
-
f'best[height<={height}]/' # TRY COMBINED FORMATS
|
|
287
|
-
'best' # FALLBACK TO BEST AVAILABLE
|
|
288
|
-
) if format == 'mp4' else 'bestaudio/best',
|
|
289
|
-
'postprocessors': [{
|
|
290
|
-
'key': 'FFmpegExtractAudio',
|
|
291
|
-
'preferredcodec': 'mp3',
|
|
292
|
-
'preferredquality': '192',
|
|
293
|
-
}] if format == 'mp3' else [],
|
|
294
|
-
'quiet': True,
|
|
295
|
-
'no_warnings': True,
|
|
296
|
-
'progress': True,
|
|
297
|
-
'progress_hooks': [lambda d: update_progress(d)],
|
|
298
|
-
'extractor_args': {
|
|
299
|
-
'youtube': {
|
|
300
|
-
'player_client': ['android', 'web'], # USE ANDROID FIRST AND WEB PLAYER CLIENTS IF ANDROID FAILS
|
|
301
|
-
'formats': ['missing_pot'], # ALLOW FORMATS WITHOUT PO TOKEN
|
|
302
|
-
'player_skip': ['configs', 'webpage'] # SKIP UNNECESSARY CONFIGS
|
|
303
|
-
}
|
|
304
|
-
},
|
|
305
|
-
'http_headers': {
|
|
306
|
-
'User-Agent': 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
|
|
307
|
-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
308
|
-
'Accept-Language': 'en-us,en;q=0.5',
|
|
309
|
-
'Sec-Fetch-Mode': 'navigate'
|
|
310
|
-
},
|
|
311
|
-
'outtmpl': str(download_dir / '%(title)s.%(ext)s'), # SET OUTPUT TEMPLATE
|
|
312
|
-
'overwrites': True, # FORCE OVERWRITE IF USER CONSENTED
|
|
313
|
-
'no_check_certificates': True, # SKIP CERTIFICATE VALIDATION
|
|
314
|
-
'ignoreerrors': False, # CATCH ERRORS PROPERLY
|
|
315
|
-
'cookiesfrombrowser': None, # DISABLE COOKIE FILE
|
|
316
|
-
'cookiefile': None, # DISABLE COOKIE FILE
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
try:
|
|
320
|
-
# THEN DOWNLOAD
|
|
321
|
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
322
|
-
try:
|
|
323
|
-
ydl.download([url])
|
|
324
|
-
safe_print("\nV Download completed successfully!")
|
|
325
|
-
return True
|
|
326
|
-
except Exception as e:
|
|
327
|
-
if "HTTP Error 403" in str(e):
|
|
328
|
-
safe_print("\n! Access denied for requested quality. Trying lower quality...")
|
|
329
|
-
# TRY DOWNLOADING WITH LOWER QUALITY
|
|
330
|
-
if height > 720:
|
|
331
|
-
new_height = min(height - 360, 1080) # STEP DOWN QUALITY
|
|
332
|
-
safe_print(f"v Falling back to {new_height}p")
|
|
333
|
-
ydl_opts['format'] = (
|
|
334
|
-
f'bestvideo[height<={new_height}][ext=mp4]+bestaudio[ext=m4a]/'
|
|
335
|
-
f'bestvideo[height<={new_height}][ext=webm]+bestaudio[ext=webm]/'
|
|
336
|
-
f'best[height<={new_height}]/'
|
|
337
|
-
'best'
|
|
338
|
-
)
|
|
339
|
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl2:
|
|
340
|
-
ydl2.download([url])
|
|
341
|
-
safe_print("\nV Download completed successfully!")
|
|
342
|
-
return True
|
|
343
|
-
else:
|
|
344
|
-
safe_print("\nX Failed to download video at any quality")
|
|
345
|
-
return False
|
|
346
|
-
else:
|
|
347
|
-
raise e
|
|
348
|
-
|
|
349
|
-
# CATCH ANY EXCEPTIONS AND HANDLE THEM
|
|
350
|
-
except Exception as e:
|
|
351
|
-
error_msg = str(e)
|
|
352
|
-
if "Requested format is not available" in error_msg:
|
|
353
|
-
print("\n❌ Format not available. Available formats are:")
|
|
354
|
-
for f in formats:
|
|
355
|
-
print(f"- {f.get('format_id', 'N/A')}: {f.get('ext', 'N/A')} ({f.get('format_note', 'N/A')})")
|
|
356
|
-
else:
|
|
357
|
-
print(f"\n❌ ERROR: {error_msg}")
|
|
358
|
-
return False
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
# FUNCTION TO LIST AVAILABLE FORMATS FOR A YOUTUBE VIDEO
|
|
362
|
-
def list_available_formats(url):
|
|
363
|
-
try:
|
|
364
|
-
with yt_dlp.YoutubeDL({'quiet': True}) as ydl:
|
|
365
|
-
info_dict = ydl.extract_info(url, download=False)
|
|
366
|
-
formats = info_dict.get('formats', None)
|
|
367
|
-
if formats:
|
|
368
|
-
for f in formats:
|
|
369
|
-
print(f"Format: {f['format_id']}, Resolution: {f.get('resolution')}, Extension: {f['ext']}")
|
|
370
|
-
except yt_dlp.utils.DownloadError as e:
|
|
371
|
-
print(f"Error fetching formats: {e}")
|
|
372
|
-
except Exception as e:
|
|
373
|
-
print(f"Unexpected error: {e}")
|
|
374
|
-
|
|
375
|
-
pbar = None # GLOBAL VARIABLE TO STORE PROGRESS BAR
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
# FUNCTION TO UPDATE PROGRESS BAR
|
|
379
|
-
def update_progress(d):
|
|
380
|
-
global pbar
|
|
381
|
-
if d['status'] == 'downloading':
|
|
382
|
-
total = d.get('total_bytes', 0)
|
|
383
|
-
downloaded = d.get('downloaded_bytes', 0)
|
|
384
|
-
|
|
385
|
-
if pbar is None:
|
|
386
|
-
pbar = tqdm(total=total, unit='B', unit_scale=True, desc="⏳ Downloading", leave=True, ncols=80, bar_format='{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} [{rate_fmt}]')
|
|
387
|
-
|
|
388
|
-
if total > 0:
|
|
389
|
-
pbar.n = downloaded
|
|
390
|
-
pbar.total = total
|
|
391
|
-
pbar.refresh()
|
|
392
|
-
|
|
393
|
-
elif d['status'] == 'finished' and pbar:
|
|
394
|
-
pbar.close()
|
|
395
|
-
pbar = None
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
# FUNCTION TO DOWNLOAD FILE WITH SPECIFIC HANDLING AND FOLDER OPENING
|
|
399
|
-
def download_file_with_tqdm(url):
|
|
400
|
-
download_dir = get_default_download_dir()
|
|
401
|
-
filename = get_filename_from_url(url)
|
|
402
|
-
dest_file = download_dir / filename
|
|
403
|
-
|
|
404
|
-
try:
|
|
405
|
-
with requests.get(url, stream=True) as response:
|
|
406
|
-
response.raise_for_status()
|
|
407
|
-
|
|
408
|
-
total_size = int(response.headers.get('content-length', 0))
|
|
409
|
-
block_size = 1024 # 1KB
|
|
410
|
-
|
|
411
|
-
with tqdm(total=total_size if total_size else None, unit='iB', unit_scale=True, desc=f"Downloading {filename}", leave=True) as tqdm_bar:
|
|
412
|
-
with open(dest_file, 'wb') as file:
|
|
413
|
-
for chunk in response.iter_content(chunk_size=block_size):
|
|
414
|
-
if chunk:
|
|
415
|
-
file.write(chunk)
|
|
416
|
-
tqdm_bar.update(len(chunk))
|
|
417
|
-
|
|
418
|
-
# AUTOMATICALLY OPEN DOWNLOAD FOLDER AFTER FILE DOWNLOAD IS COMPLETE
|
|
419
|
-
open_download_folder(download_dir)
|
|
420
|
-
except requests.exceptions.RequestException as e:
|
|
421
|
-
print(f"Error during file download: {e}")
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
# FUNCTION TO GET BROWSER COOKIES
|
|
425
|
-
def get_browser_cookies():
|
|
426
|
-
"""GET BROWSER COOKIES WITH FALLBACK OPTIONS"""
|
|
427
|
-
try:
|
|
428
|
-
return ('chrome',) # TRY CHROME FIRST
|
|
429
|
-
except Exception:
|
|
430
|
-
try:
|
|
431
|
-
return ('firefox',) # TRY FIREFOX IF CHROME FAILS
|
|
432
|
-
except Exception:
|
|
433
|
-
return None # RETURN NONE IF BOTH FAIL
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# INIT FILE FOR TESTS
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from unittest.mock import patch, Mock
|
|
3
|
-
from autotools.autoip.core import get_public_ip, get_local_ip, get_ip_info
|
|
4
|
-
|
|
5
|
-
# MOCK DATA
|
|
6
|
-
MOCK_IP_INFO = {
|
|
7
|
-
'ip': '8.8.8.8',
|
|
8
|
-
'city': 'Mountain View',
|
|
9
|
-
'region': 'California',
|
|
10
|
-
'country': 'US',
|
|
11
|
-
'loc': '37.4056,-122.0775',
|
|
12
|
-
'org': 'Google LLC',
|
|
13
|
-
'timezone': 'America/Los_Angeles'
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
# UNIT TESTS
|
|
17
|
-
|
|
18
|
-
# TEST FOR PUBLIC IP RETRIEVAL
|
|
19
|
-
@patch('requests.get')
|
|
20
|
-
def test_get_public_ip(mock_get):
|
|
21
|
-
"""TEST PUBLIC IP RETRIEVAL"""
|
|
22
|
-
mock_get.return_value.text = "1.2.3.4"
|
|
23
|
-
ip = get_public_ip()
|
|
24
|
-
assert ip == "1.2.3.4"
|
|
25
|
-
mock_get.assert_called_once()
|
|
26
|
-
|
|
27
|
-
# TEST FOR LOCAL IP RETRIEVAL
|
|
28
|
-
@patch('socket.socket')
|
|
29
|
-
@patch('netifaces.gateways')
|
|
30
|
-
@patch('netifaces.ifaddresses')
|
|
31
|
-
def test_get_local_ip(mock_ifaddresses, mock_gateways, mock_socket):
|
|
32
|
-
"""TEST LOCAL IP RETRIEVAL"""
|
|
33
|
-
# MOCK NETIFACES
|
|
34
|
-
mock_gateways.return_value = {'default': {2: ('192.168.1.1', 'eth0')}}
|
|
35
|
-
mock_ifaddresses.return_value = {2: [{'addr': '192.168.1.100'}]}
|
|
36
|
-
|
|
37
|
-
ip = get_local_ip()
|
|
38
|
-
assert ip == "192.168.1.100"
|
|
39
|
-
|
|
40
|
-
# TEST FOR IP INFO RETRIEVAL
|
|
41
|
-
@patch('requests.get')
|
|
42
|
-
def test_get_ip_info(mock_get):
|
|
43
|
-
"""TEST IP INFO RETRIEVAL"""
|
|
44
|
-
mock_get.return_value.json.return_value = MOCK_IP_INFO
|
|
45
|
-
info = get_ip_info()
|
|
46
|
-
assert isinstance(info, dict)
|
|
47
|
-
assert info == MOCK_IP_INFO
|
|
48
|
-
|
|
49
|
-
# TEST FOR IP INFO WITH SPECIFIC IP
|
|
50
|
-
@patch('requests.get')
|
|
51
|
-
def test_get_ip_info_with_ip(mock_get):
|
|
52
|
-
"""TEST IP INFO WITH SPECIFIC IP"""
|
|
53
|
-
mock_get.return_value.json.return_value = MOCK_IP_INFO
|
|
54
|
-
test_ip = "8.8.8.8" # GOOGLE DNS
|
|
55
|
-
info = get_ip_info(test_ip)
|
|
56
|
-
assert isinstance(info, dict)
|
|
57
|
-
assert info['ip'] == test_ip
|
|
58
|
-
assert 'Google' in info['org']
|
|
59
|
-
|
|
60
|
-
# TEST FOR IP INFO WITH INVALID IP
|
|
61
|
-
def test_get_ip_info_invalid():
|
|
62
|
-
"""TEST IP INFO WITH INVALID IP"""
|
|
63
|
-
with pytest.raises(ValueError):
|
|
64
|
-
get_ip_info("invalid.ip.address")
|
|
65
|
-
|
|
66
|
-
# TEST FOR IP INFO WITH PRIVATE IP
|
|
67
|
-
def test_get_ip_info_private():
|
|
68
|
-
"""TEST IP INFO WITH PRIVATE IP"""
|
|
69
|
-
private_ips = ["192.168.1.1", "10.0.0.1", "172.16.0.1"]
|
|
70
|
-
for ip in private_ips:
|
|
71
|
-
with pytest.raises(ValueError):
|
|
72
|
-
get_ip_info(ip)
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from unittest.mock import patch, Mock
|
|
3
|
-
from click.testing import CliRunner
|
|
4
|
-
from autotools.cli import autoip
|
|
5
|
-
|
|
6
|
-
# MOCK DATA
|
|
7
|
-
MOCK_IP_INFO = {
|
|
8
|
-
'ip': '8.8.8.8',
|
|
9
|
-
'city': 'Mountain View',
|
|
10
|
-
'region': 'California',
|
|
11
|
-
'country': 'US',
|
|
12
|
-
'loc': '37.4056,-122.0775',
|
|
13
|
-
'org': 'Google LLC',
|
|
14
|
-
'timezone': 'America/Los_Angeles'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
# INTEGRATION TESTS
|
|
18
|
-
|
|
19
|
-
# TEST FOR BASIC CLI FUNCTIONALITY
|
|
20
|
-
@patch('autotools.autoip.core.get_local_ips')
|
|
21
|
-
@patch('autotools.autoip.core.get_public_ips')
|
|
22
|
-
def test_autoip_cli_basic(mock_public_ips, mock_local_ips):
|
|
23
|
-
"""TEST BASIC CLI FUNCTIONALITY"""
|
|
24
|
-
mock_local_ips.return_value = {
|
|
25
|
-
'ipv4': ['192.168.1.100'],
|
|
26
|
-
'ipv6': ['fe80::1']
|
|
27
|
-
}
|
|
28
|
-
mock_public_ips.return_value = {
|
|
29
|
-
'ipv4': '1.2.3.4',
|
|
30
|
-
'ipv6': None
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
runner = CliRunner()
|
|
34
|
-
result = runner.invoke(autoip)
|
|
35
|
-
assert result.exit_code == 0
|
|
36
|
-
assert "192.168.1.100" in result.output
|
|
37
|
-
assert "1.2.3.4" in result.output
|
|
38
|
-
assert "fe80::1" in result.output
|
|
39
|
-
|
|
40
|
-
# TEST FOR CONNECTIVITY TEST
|
|
41
|
-
@patch('autotools.autoip.core.test_connectivity')
|
|
42
|
-
def test_autoip_cli_test(mock_test):
|
|
43
|
-
"""TEST CONNECTIVITY TEST"""
|
|
44
|
-
mock_test.return_value = [
|
|
45
|
-
('Google DNS', True, 20),
|
|
46
|
-
('CloudFlare', False, None)
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
runner = CliRunner()
|
|
50
|
-
result = runner.invoke(autoip, ['--test'])
|
|
51
|
-
assert result.exit_code == 0
|
|
52
|
-
assert "Google DNS" in result.output
|
|
53
|
-
assert "CloudFlare" in result.output
|
|
54
|
-
assert "✓ 20ms" in result.output
|
|
55
|
-
assert "✗ Failed" in result.output
|
|
56
|
-
|
|
57
|
-
# TEST FOR SPEED TEST
|
|
58
|
-
@patch('autotools.autoip.core.run_speedtest')
|
|
59
|
-
def test_autoip_cli_speed(mock_speed):
|
|
60
|
-
"""TEST SPEED TEST"""
|
|
61
|
-
mock_speed.return_value = True
|
|
62
|
-
|
|
63
|
-
runner = CliRunner()
|
|
64
|
-
result = runner.invoke(autoip, ['--speed'])
|
|
65
|
-
assert result.exit_code == 0
|
|
66
|
-
assert "Running speed test" in result.output
|
|
67
|
-
assert "completed successfully" in result.output
|
|
68
|
-
|
|
69
|
-
# TEST FOR LOCATION INFO DISPLAY
|
|
70
|
-
@patch('autotools.autoip.core.get_ip_info')
|
|
71
|
-
def test_autoip_cli_location(mock_get_info):
|
|
72
|
-
"""TEST LOCATION INFO DISPLAY"""
|
|
73
|
-
mock_get_info.return_value = MOCK_IP_INFO
|
|
74
|
-
|
|
75
|
-
runner = CliRunner()
|
|
76
|
-
result = runner.invoke(autoip, ['--location'])
|
|
77
|
-
assert result.exit_code == 0
|
|
78
|
-
assert "Mountain View" in result.output
|
|
79
|
-
assert "California" in result.output
|
|
80
|
-
assert "Google LLC" in result.output
|
|
81
|
-
|
|
82
|
-
# TEST FOR HELP DISPLAY
|
|
83
|
-
def test_autoip_cli_help():
|
|
84
|
-
"""TEST HELP DISPLAY"""
|
|
85
|
-
runner = CliRunner()
|
|
86
|
-
result = runner.invoke(autoip, ['--help'])
|
|
87
|
-
assert result.exit_code == 0
|
|
88
|
-
assert "Usage:" in result.output
|
|
89
|
-
assert "Options:" in result.output
|
|
90
|
-
assert "--test" in result.output
|
|
91
|
-
assert "--speed" in result.output
|
|
92
|
-
assert "--location" in result.output
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# INIT FILE FOR TESTS
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from autotools.autolower.core import autolower_transform
|
|
3
|
-
|
|
4
|
-
# UNIT TESTS
|
|
5
|
-
|
|
6
|
-
# TEST FOR BASIC STRING TRANSFORMATION
|
|
7
|
-
def test_autolower_transform_basic():
|
|
8
|
-
"""TEST BASIC STRING TRANSFORMATION"""
|
|
9
|
-
assert autolower_transform("HELLO") == "hello"
|
|
10
|
-
assert autolower_transform("Hello World") == "hello world"
|
|
11
|
-
assert autolower_transform("123") == "123"
|
|
12
|
-
|
|
13
|
-
# TEST FOR EMPTY STRING
|
|
14
|
-
def test_autolower_transform_empty():
|
|
15
|
-
"""TEST EMPTY STRING"""
|
|
16
|
-
assert autolower_transform("") == ""
|
|
17
|
-
|
|
18
|
-
# TEST FOR SPECIAL CHARACTERS
|
|
19
|
-
def test_autolower_transform_special_chars():
|
|
20
|
-
"""TEST STRING WITH SPECIAL CHARACTERS"""
|
|
21
|
-
assert autolower_transform("HELLO@WORLD.COM") == "hello@world.com"
|
|
22
|
-
assert autolower_transform("HELLO-WORLD!") == "hello-world!"
|
|
23
|
-
|
|
24
|
-
# TEST FOR MIXED CASE STRING
|
|
25
|
-
def test_autolower_transform_mixed_case():
|
|
26
|
-
"""TEST MIXED CASE STRING"""
|
|
27
|
-
assert autolower_transform("HeLLo WoRLD") == "hello world"
|
|
28
|
-
|
|
29
|
-
# TEST FOR WHITESPACE
|
|
30
|
-
def test_autolower_transform_whitespace():
|
|
31
|
-
"""TEST STRING WITH WHITESPACE"""
|
|
32
|
-
assert autolower_transform(" HELLO WORLD ") == " hello world "
|
|
33
|
-
assert autolower_transform("\tHELLO\nWORLD") == "\thello\nworld"
|
|
34
|
-
|
|
35
|
-
# TEST FOR NUMBERS
|
|
36
|
-
def test_autolower_transform_numbers():
|
|
37
|
-
"""TEST STRING WITH NUMBERS"""
|
|
38
|
-
assert autolower_transform("HELLO123WORLD") == "hello123world"
|
|
39
|
-
assert autolower_transform("123HELLO456WORLD789") == "123hello456world789"
|
|
40
|
-
|
|
41
|
-
# TEST FOR UNICODE CHARACTERS
|
|
42
|
-
def test_autolower_transform_unicode():
|
|
43
|
-
"""TEST UNICODE CHARACTERS"""
|
|
44
|
-
assert autolower_transform("HÉLLO WÖRLD") == "héllo wörld"
|
|
45
|
-
assert autolower_transform("こんにちは") == "こんにちは" # JAPANESE SHOULD REMAIN UNCHANGED
|