savehub-cli 1.0.0__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.
@@ -0,0 +1,8 @@
1
+ """
2
+ SaveHub CLI - Multilingual Social Media Downloader
3
+ Supports: Azərbaycanca (AZ), English (EN), Türkçe (TR)
4
+ """
5
+
6
+ __version__ = "1.0.0"
7
+ __author__ = "Sadiq Musayev"
8
+ __license__ = "MIT"
savehub_cli/config.py ADDED
@@ -0,0 +1,25 @@
1
+ # SaveHub CLI - Configuration
2
+
3
+ LANGUAGES = ['AZ', 'EN', 'TR']
4
+
5
+ PLATFORM_CONFIG = {
6
+ 'YouTube': {'patterns': ['youtube.com', 'youtu.be'], 'supports': ['video', 'playlist', 'audio']},
7
+ 'Instagram': {'patterns': ['instagram.com'], 'supports': ['video', 'photo']},
8
+ 'Facebook': {'patterns': ['facebook.com', 'fb.watch'], 'supports': ['video']},
9
+ 'Twitter/X': {'patterns': ['twitter.com', 'x.com'], 'supports': ['video', 'gif']},
10
+ 'LinkedIn': {'patterns': ['linkedin.com'], 'supports': ['video']},
11
+ 'Snapchat': {'patterns': ['snapchat.com'], 'supports': ['video']},
12
+ }
13
+
14
+ FORMAT_SETTINGS = {
15
+ 'date_format': '%Y-%m-%d',
16
+ 'time_format': '%H:%M:%S',
17
+ 'log_filename_format': '%Y-%m-%d',
18
+ }
19
+
20
+ DOWNLOAD_SETTINGS = {
21
+ 'output_dir': 'downloads',
22
+ 'log_dir': 'logs',
23
+ 'mp3_quality': '192',
24
+ 'default_format': 'mp4',
25
+ }
savehub_cli/main.py ADDED
@@ -0,0 +1,73 @@
1
+ """
2
+ SaveHub CLI - Main Entry Point
3
+ Multilingual Multimedia Downloader
4
+ Supports: Azərbaycanca (AZ), English (EN), Türkçe (TR)
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ from pathlib import Path
10
+
11
+
12
+ def main():
13
+ """Main application entry point — called by pip-installed console script."""
14
+
15
+ # Ensure working directories exist in user's CWD (not package dir)
16
+ Path('downloads').mkdir(exist_ok=True)
17
+ Path('logs').mkdir(exist_ok=True)
18
+
19
+ from savehub_cli.modules.menu import select_language, show_main_menu, display_header
20
+ from savehub_cli.modules.menu import show_documentation, show_terms, show_feedback
21
+ from savehub_cli.modules.downloader import start_download
22
+ from savehub_cli.translations import get_text
23
+
24
+ try:
25
+ language = select_language()
26
+
27
+ while True:
28
+ os.system('cls' if os.name == 'nt' else 'clear')
29
+ display_header(language)
30
+
31
+ choice = show_main_menu(language)
32
+
33
+ if choice == 'option_docs':
34
+ os.system('cls' if os.name == 'nt' else 'clear')
35
+ show_documentation(language)
36
+ input(f"\n{get_text('press_enter', language)}")
37
+
38
+ elif choice == 'option_terms':
39
+ os.system('cls' if os.name == 'nt' else 'clear')
40
+ show_terms(language)
41
+ input(f"\n{get_text('press_enter', language)}")
42
+
43
+ elif choice == 'option_feedback':
44
+ os.system('cls' if os.name == 'nt' else 'clear')
45
+ show_feedback(language)
46
+ input(f"\n{get_text('press_enter', language)}")
47
+
48
+ elif choice == 'option_start':
49
+ start_download(language)
50
+
51
+ elif choice == 'option_exit':
52
+ os.system('cls' if os.name == 'nt' else 'clear')
53
+ print(f"\n{get_text('goodbye', language)}")
54
+ print(f"{get_text('see_you', language)}\n")
55
+ break
56
+
57
+ except (KeyboardInterrupt, EOFError):
58
+ print("\n\n👋 Goodbye!")
59
+ sys.exit(0)
60
+ except Exception as e:
61
+ import traceback
62
+ log_msg = traceback.format_exc()
63
+ try:
64
+ from savehub_cli.modules.utils import log_event
65
+ log_event(f"Unhandled exception: {log_msg}", 'ERROR')
66
+ except Exception:
67
+ pass
68
+ print("\n❌ Unexpected error occurred. Check logs for details.")
69
+ sys.exit(1)
70
+
71
+
72
+ if __name__ == "__main__":
73
+ main()
@@ -0,0 +1,12 @@
1
+ """
2
+ SaveHub CLI - Modules Package
3
+ """
4
+
5
+ __version__ = '1.0.0'
6
+ __author__ = 'Sadiq Musayev'
7
+
8
+ from . import downloader
9
+ from . import menu
10
+ from . import utils
11
+
12
+ __all__ = ['downloader', 'menu', 'utils']
@@ -0,0 +1,415 @@
1
+ """
2
+ SaveHub CLI - Download Module
3
+ Uses yt-dlp for multimedia downloading
4
+ Database-ready download logging structure
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import threading
10
+ import time
11
+ from datetime import datetime
12
+ from questionary import text as questionary_text, select
13
+ import yt_dlp
14
+ from savehub_cli.translations import get_text
15
+ import logging
16
+ from savehub_cli.modules.utils import validate_url, get_platform_from_url, log_event
17
+
18
+ # Suppress all console log output — logs go to file only
19
+ logging.getLogger().setLevel(logging.CRITICAL)
20
+
21
+
22
+ # ─────────────────────────────────────────────
23
+ # Database-ready download record structure
24
+ # TODO: Replace with actual DB insert when DB is connected
25
+ # ─────────────────────────────────────────────
26
+ def create_download_record(url, platform, format_type, filename, status,
27
+ error=None, duration_sec=None, file_size_bytes=None):
28
+ """
29
+ Creates a structured download record.
30
+ Ready to be inserted into a database table:
31
+
32
+ Table: downloads
33
+ ┌──────────────────┬──────────────┐
34
+ │ Column │ Type │
35
+ ├──────────────────┼──────────────┤
36
+ │ id │ INTEGER PK │
37
+ │ url │ TEXT │
38
+ │ platform │ TEXT │
39
+ │ format_type │ TEXT │
40
+ │ filename │ TEXT │
41
+ │ status │ TEXT │
42
+ │ error │ TEXT NULL │
43
+ │ duration_sec │ REAL NULL │
44
+ │ file_size_bytes │ INTEGER NULL │
45
+ │ created_at │ DATETIME │
46
+ └──────────────────┴──────────────┘
47
+ """
48
+ record = {
49
+ 'url': url,
50
+ 'platform': platform,
51
+ 'format_type': format_type,
52
+ 'filename': filename,
53
+ 'status': status, # 'success' | 'failed' | 'paused' | 'cancelled'
54
+ 'error': error,
55
+ 'duration_sec': duration_sec,
56
+ 'file_size_bytes': file_size_bytes,
57
+ 'created_at': datetime.now().isoformat(),
58
+ }
59
+
60
+ # TODO: db.insert('downloads', record)
61
+ log_event(f"[DB_RECORD] {record}")
62
+ return record
63
+
64
+
65
+ # ─────────────────────────────────────────────
66
+ # Download state manager — pause / stop / cancel
67
+ # ─────────────────────────────────────────────
68
+ class DownloadController:
69
+ """
70
+ Thread-safe controller for pause / resume / cancel.
71
+
72
+ States:
73
+ running → download in progress
74
+ paused → download suspended (S key)
75
+ cancelled → user chose to terminate (C key)
76
+ """
77
+
78
+ def __init__(self):
79
+ self._state = 'running'
80
+ self._lock = threading.Lock()
81
+
82
+ @property
83
+ def is_paused(self):
84
+ with self._lock:
85
+ return self._state == 'paused'
86
+
87
+ @property
88
+ def is_cancelled(self):
89
+ with self._lock:
90
+ return self._state == 'cancelled'
91
+
92
+ @property
93
+ def is_running(self):
94
+ with self._lock:
95
+ return self._state == 'running'
96
+
97
+ @property
98
+ def state(self):
99
+ with self._lock:
100
+ return self._state
101
+
102
+ def pause(self):
103
+ with self._lock:
104
+ if self._state == 'running':
105
+ self._state = 'paused'
106
+
107
+ def resume(self):
108
+ with self._lock:
109
+ if self._state == 'paused':
110
+ self._state = 'running'
111
+
112
+ def cancel(self):
113
+ with self._lock:
114
+ self._state = 'cancelled'
115
+
116
+ def toggle_pause(self):
117
+ with self._lock:
118
+ if self._state == 'running':
119
+ self._state = 'paused'
120
+ elif self._state == 'paused':
121
+ self._state = 'running'
122
+
123
+
124
+ # ─────────────────────────────────────────────
125
+ # Keyboard listener thread (cross-platform)
126
+ # ─────────────────────────────────────────────
127
+ def _start_key_listener(controller: DownloadController):
128
+ """
129
+ Listens for keypresses in a background thread:
130
+ S / s → pause / resume
131
+ C / c → cancel
132
+ Works on Windows (msvcrt) and Unix (tty/termios).
133
+ """
134
+ def _listen():
135
+ if sys.platform == 'win32':
136
+ import msvcrt
137
+ while not controller.is_cancelled:
138
+ if msvcrt.kbhit():
139
+ key = msvcrt.getwch().lower()
140
+ if key == 's':
141
+ controller.toggle_pause()
142
+ elif key == 'c':
143
+ controller.cancel()
144
+ break
145
+ time.sleep(0.05)
146
+ else:
147
+ import tty
148
+ import termios
149
+ fd = sys.stdin.fileno()
150
+ old = termios.tcgetattr(fd)
151
+ try:
152
+ tty.setraw(fd)
153
+ while not controller.is_cancelled:
154
+ ch = sys.stdin.read(1).lower()
155
+ if ch == 's':
156
+ controller.toggle_pause()
157
+ elif ch == 'c':
158
+ controller.cancel()
159
+ break
160
+ finally:
161
+ termios.tcsetattr(fd, termios.TCSADRAIN, old)
162
+
163
+ t = threading.Thread(target=_listen, daemon=True)
164
+ t.start()
165
+ return t
166
+
167
+
168
+ # ─────────────────────────────────────────────
169
+ # Progress bar with pause/cancel controls
170
+ # ─────────────────────────────────────────────
171
+ class ProgressBar:
172
+ """
173
+ Clean terminal progress bar.
174
+ Shows controls: [S] Stop/Resume [C] Cancel
175
+ """
176
+
177
+ BAR_WIDTH = 34
178
+
179
+ def __init__(self, controller: DownloadController):
180
+ self.controller = controller
181
+ self.current_item = 0
182
+ self.total_items = 1
183
+ self._last_line = ''
184
+
185
+ def hook(self, d):
186
+ while self.controller.is_paused:
187
+ self._render(paused=True)
188
+ time.sleep(0.3)
189
+
190
+ if self.controller.is_cancelled:
191
+ raise yt_dlp.utils.DownloadCancelled()
192
+
193
+ if d['status'] == 'downloading':
194
+ raw = d.get('_percent_str', '0%').strip()
195
+ raw = ''.join(c for c in raw if c.isdigit() or c in '.%')
196
+ try:
197
+ percent = float(raw.replace('%', ''))
198
+ except ValueError:
199
+ percent = 0.0
200
+
201
+ speed = d.get('_speed_str', '').strip() or '?'
202
+ eta = d.get('_eta_str', '').strip() or '?'
203
+ size = d.get('_total_bytes_str',
204
+ d.get('_total_bytes_estimate_str', '?')).strip()
205
+
206
+ self._render(percent=percent, speed=speed, eta=eta, size=size)
207
+
208
+ elif d['status'] == 'finished':
209
+ print()
210
+
211
+ def _render(self, percent=0.0, speed='', eta='', size='', paused=False):
212
+ filled = int(self.BAR_WIDTH * percent / 100)
213
+ bar = '█' * filled + '░' * (self.BAR_WIDTH - filled)
214
+
215
+ item_info = (
216
+ f" [{self.current_item}/{self.total_items}]"
217
+ if self.total_items > 1 else ""
218
+ )
219
+
220
+ if paused:
221
+ status_icon = '⏸ PAUSED '
222
+ right = ' [S] Resume [C] Cancel'
223
+ else:
224
+ status_icon = '▶ '
225
+ right = ' [S] Stop [C] Cancel'
226
+
227
+ line = (
228
+ f"\r {status_icon}{bar} {percent:5.1f}%"
229
+ f" {size} {speed} ETA {eta}{item_info}{right} "
230
+ )
231
+ print(line, end='', flush=True)
232
+ self._last_line = line
233
+
234
+
235
+ def show_format_menu(lang='en'):
236
+ """Show format selection menu"""
237
+ if lang == 'az':
238
+ choices = [
239
+ '1️⃣ MP4 (Video+Audio) - Səs və video ilə indir',
240
+ '2️⃣ MP3 (Səs) - Yalnız səs indir',
241
+ '3️⃣ Silent Video - Yalnız video (səssiz) indir',
242
+ ]
243
+ elif lang == 'tr':
244
+ choices = [
245
+ '1️⃣ MP4 (Video+Ses) - Ses ve video ile indir',
246
+ '2️⃣ MP3 (Ses) - Sadece ses indir',
247
+ '3️⃣ Sessiz Video - Sadece video indir (ses yok)',
248
+ ]
249
+ else:
250
+ choices = [
251
+ '1️⃣ MP4 (Video+Audio) - Download with audio and video',
252
+ '2️⃣ MP3 (Audio) - Download audio only',
253
+ '3️⃣ Silent Video - Download video only (no audio)',
254
+ ]
255
+
256
+ answer = select(
257
+ get_text('select_format', lang),
258
+ choices=choices,
259
+ use_arrow_keys=True
260
+ ).ask()
261
+
262
+ if answer is None:
263
+ return 'mp4'
264
+ if '1️⃣' in answer:
265
+ return 'mp4'
266
+ elif '2️⃣' in answer:
267
+ return 'mp3'
268
+ else:
269
+ return 'silent'
270
+
271
+
272
+ def download_media(url: str, format_type: str, lang: str = 'en') -> bool:
273
+ """Download media from URL with pause/resume/cancel support."""
274
+
275
+ if not validate_url(url):
276
+ msgs = {'az': '❌ Düzgün olmayan URL formatı!',
277
+ 'tr': '❌ Geçersiz URL formatı!'}
278
+ print(f"\n{msgs.get(lang, '❌ Invalid URL format!')}")
279
+ return False
280
+
281
+ platform = get_platform_from_url(url)
282
+ if not platform:
283
+ msgs = {'az': '❌ Dəstəkləməyən platforma!',
284
+ 'tr': '❌ Desteklenmeyen platform!'}
285
+ print(f"\n{msgs.get(lang, '❌ Unsupported platform!')}")
286
+ return False
287
+
288
+ print(f"\n Platform : {platform}")
289
+ print(f" Format : {format_type.upper()}")
290
+ print(f" {get_text('downloading', lang)}")
291
+ print(f" Controls : [S] Stop/Resume [C] Cancel\n")
292
+
293
+ os.makedirs('downloads', exist_ok=True)
294
+
295
+ controller = DownloadController()
296
+ progress = ProgressBar(controller)
297
+ saved_files = []
298
+ start_time = time.time()
299
+
300
+ _start_key_listener(controller)
301
+
302
+ ydl_opts = {
303
+ 'outtmpl': 'downloads/%(title)s.%(ext)s',
304
+ 'quiet': True,
305
+ 'no_warnings': True,
306
+ 'noprogress': True,
307
+ 'progress_hooks': [progress.hook],
308
+ }
309
+
310
+ if format_type == 'mp4':
311
+ ydl_opts['format'] = 'best[ext=mp4]/best'
312
+ elif format_type == 'mp3':
313
+ ydl_opts['format'] = 'bestaudio/best'
314
+ ydl_opts['postprocessors'] = [{
315
+ 'key': 'FFmpegExtractAudio',
316
+ 'preferredcodec': 'mp3',
317
+ 'preferredquality': '192',
318
+ }]
319
+ elif format_type == 'silent':
320
+ ydl_opts['format'] = 'bestvideo[ext=mp4]/bestvideo'
321
+
322
+ info_opts = dict(ydl_opts)
323
+ info_opts['quiet'] = True
324
+ info_opts['no_warnings'] = True
325
+ info_opts['extract_flat'] = 'in_playlist'
326
+ info_opts['progress_hooks'] = []
327
+
328
+ try:
329
+ with yt_dlp.YoutubeDL(info_opts) as ydl_info:
330
+ meta = ydl_info.extract_info(url, download=False)
331
+ if meta and 'entries' in meta:
332
+ entries = [e for e in meta['entries'] if e]
333
+ progress.total_items = len(entries)
334
+ print(f" Playlist : {meta.get('title', '')} ({progress.total_items} items)\n")
335
+ else:
336
+ progress.total_items = 1
337
+
338
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
339
+ info = ydl.extract_info(url, download=True)
340
+
341
+ if 'entries' in info:
342
+ for entry in info['entries']:
343
+ if entry:
344
+ saved_files.append(ydl.prepare_filename(entry))
345
+ else:
346
+ saved_files.append(ydl.prepare_filename(info))
347
+
348
+ duration = round(time.time() - start_time, 2)
349
+ download_dir = os.path.abspath('downloads')
350
+
351
+ print(f"\n ✅ {get_text('download_complete', lang)}")
352
+ print(f" 📂 {download_dir}")
353
+ if len(saved_files) == 1:
354
+ fname = saved_files[0]
355
+ fsize = os.path.getsize(fname) if os.path.exists(fname) else None
356
+ print(f" 📄 {os.path.basename(fname)}")
357
+ if fsize:
358
+ print(f" 💾 {fsize / (1024*1024):.2f} MB ⏱ {duration}s")
359
+ create_download_record(url, platform, format_type, fname,
360
+ 'success', duration_sec=duration,
361
+ file_size_bytes=fsize)
362
+ else:
363
+ print(f" 📄 {len(saved_files)} files saved ⏱ {duration}s")
364
+ for f in saved_files:
365
+ fsize = os.path.getsize(f) if os.path.exists(f) else None
366
+ create_download_record(url, platform, format_type, f,
367
+ 'success', duration_sec=duration,
368
+ file_size_bytes=fsize)
369
+
370
+ log_event(f"Download completed: {len(saved_files)} file(s) from {platform} in {duration}s")
371
+ return True
372
+
373
+ except (yt_dlp.utils.DownloadCancelled, KeyboardInterrupt):
374
+ controller.cancel()
375
+ print(f"\n\n 🚫 {get_text('download_cancelled', lang)}")
376
+ create_download_record(url, platform, format_type, '', 'cancelled')
377
+ log_event(f"Download cancelled: {url}")
378
+ return False
379
+
380
+ except Exception as e:
381
+ print(f"\n ❌ {get_text('error_download', lang)}: {str(e)}")
382
+ create_download_record(url, platform, format_type, '', 'failed', error=str(e))
383
+ log_event(f"Download error: {str(e)}", 'ERROR')
384
+ return False
385
+
386
+
387
+ def start_download(lang: str = 'en') -> None:
388
+ """Start download process"""
389
+ os.system('cls' if os.name == 'nt' else 'clear')
390
+
391
+ print("\n" + "=" * 50)
392
+ print(f" ▶️ {get_text('welcome', lang)}")
393
+ print("=" * 50 + "\n")
394
+
395
+ url = questionary_text(
396
+ get_text('enter_url', lang),
397
+ validate=lambda x: len(x.strip()) > 5 or get_text('error_invalid_url', lang)
398
+ ).ask()
399
+
400
+ if not url or not url.strip():
401
+ print(f"\n ❌ {get_text('error_no_url', lang)}")
402
+ try:
403
+ input(f"\n{get_text('press_enter', lang)}")
404
+ except (EOFError, KeyboardInterrupt):
405
+ pass
406
+ return
407
+
408
+ url = url.strip()
409
+ format_type = show_format_menu(lang)
410
+ download_media(url, format_type, lang)
411
+
412
+ try:
413
+ input(f"\n{get_text('press_enter', lang)}")
414
+ except (EOFError, KeyboardInterrupt):
415
+ pass
@@ -0,0 +1,348 @@
1
+ """
2
+ SaveHub CLI - Interactive Menu Module
3
+ Supports: Azərbaycanca, English, Türkçe
4
+ """
5
+
6
+ import os
7
+ from questionary import select
8
+ from savehub_cli.translations import get_text
9
+
10
+
11
+ def clear_screen():
12
+ """Clear terminal screen"""
13
+ os.system('cls' if os.name == 'nt' else 'clear')
14
+
15
+
16
+ def display_header(lang='en'):
17
+ """Display ASCII art header"""
18
+ header = """
19
+ ███████╗ █████╗ ██╗ ██╗███████╗██╗ ██╗██╗ ██╗██████╗
20
+ ██╔════╝██╔══██╗██║ ██║██╔════╝██║ ██║██║ ██║██╔══██╗
21
+ ███████╗███████║██║ ██║█████╗ ███████║██║ ██║██████╔╝
22
+ ╚════██╗██╔══██║╚██╗ ██╔╝██╔══╝ ██╔══██║██║ ██║██╔══██╗
23
+ ███████║██║ ██║ ╚████╔╝ ███████╗██║ ██║╚██████╔╝██████╔╝
24
+ ╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝
25
+ """
26
+ print(header)
27
+ print("[Version 1.0.0] - Made with ❤️ by Sadiq Musayev\n")
28
+
29
+
30
+ def select_language():
31
+ """Language selection menu"""
32
+ print("\n" + "=" * 50)
33
+ print("🌍 SELECT YOUR LANGUAGE / DİLİNİZİ SEÇİN / DİLİ SEÇİN")
34
+ print("=" * 50 + "\n")
35
+
36
+ choices = [
37
+ '🇦🇿 Azərbaycanca',
38
+ '🇬🇧 English',
39
+ '🇹🇷 Türkçe'
40
+ ]
41
+
42
+ answer = select(
43
+ 'Language / Dil / Dil:',
44
+ choices=choices,
45
+ use_arrow_keys=True,
46
+ use_indicator=True
47
+ ).ask()
48
+
49
+ if 'Azərbaycanca' in answer:
50
+ return 'az'
51
+ elif 'English' in answer:
52
+ return 'en'
53
+ else:
54
+ return 'tr'
55
+
56
+
57
+ _OPTION_KEYS = ['option_docs', 'option_terms', 'option_feedback', 'option_start', 'option_exit']
58
+
59
+
60
+ def show_main_menu(lang='en'):
61
+ """
62
+ Main menu display.
63
+ Returns the internal option key string (e.g. 'option_start')
64
+ so that main.py can match on it reliably — regardless of language.
65
+ """
66
+ label_to_key = {get_text(key, lang): key for key in _OPTION_KEYS}
67
+
68
+ answer = select(
69
+ get_text('main_menu_question', lang),
70
+ choices=list(label_to_key.keys()),
71
+ use_arrow_keys=True,
72
+ use_indicator=True
73
+ ).ask()
74
+
75
+ return label_to_key.get(answer, answer)
76
+
77
+
78
+ def show_documentation(lang='en'):
79
+ """Display documentation"""
80
+ if lang == 'az':
81
+ doc_text = """
82
+ ╔════════════════════════════════════════════════════════════╗
83
+ ║ 📚 SAVEHUB CLI - İSTİFADƏÇİ KƏLAVUZu ║
84
+ ╚════════════════════════════════════════════════════════════╝
85
+
86
+ 🎥 DƏSTƏKLƏNƏN PLATFORMALAR:
87
+ • YouTube - Videolar və playlist
88
+ • Instagram - Foto və videolar
89
+ • Facebook - Videolar
90
+ • X (Twitter) - Videolar və GIF
91
+ • LinkedIn - Videolar
92
+ • Snapchat - Videolar
93
+
94
+ 📊 FORMAT SEÇİMLƏRİ:
95
+ • MP4 (Video+Audio) - Səs və video ilə indir
96
+ • MP3 (Səs) - Yalnız səs indir
97
+ • Silent Video - Yalnız video (səssiz) indir
98
+
99
+ ⚙️ NECƏ İSTİFADƏ EDƏCƏKSINIZ:
100
+ 1. "Endirmə aləti başlat" seçin
101
+ 2. Media linkini daxil edin
102
+ 3. Formatı seçin
103
+ 4. Yükləmə avtomatik başlanacaq
104
+ 5. Fayllar 'downloads' qovluğunda saxlanılır
105
+
106
+ 🔒 XƏBƏRDARLIQLAR:
107
+ • Yalnız qanuni məqsədlər üçün istifadə edin
108
+ • Müəlliflik hüquqlarına hörmət edin
109
+ • Başqasının kontentini paylaşmayın
110
+
111
+ 📁 LOGLAR:
112
+ • Bütün əməliyyatlar 'logs' qovluğunda saxlanılır
113
+ • Tarih və vaxt ilə adlandırılmış fayllar
114
+ """
115
+ elif lang == 'tr':
116
+ doc_text = """
117
+ ╔════════════════════════════════════════════════════════════╗
118
+ ║ 📚 SAVEHUB CLI - KULLANICI KILAVUZu ║
119
+ ╚════════════════════════════════════════════════════════════╝
120
+
121
+ 🎥 DESTEKLENEN PLATFORMLAR:
122
+ • YouTube - Videolar ve oynatma listeleri
123
+ • Instagram - Fotoğraflar ve videolar
124
+ • Facebook - Videolar
125
+ • X (Twitter) - Videolar ve GIF'ler
126
+ • LinkedIn - Videolar
127
+ • Snapchat - Videolar
128
+
129
+ 📊 FORMAT SEÇENEKLERİ:
130
+ • MP4 (Video+Ses) - Ses ve video ile indir
131
+ • MP3 (Ses) - Sadece ses indir
132
+ • Sessiz Video - Sadece video indir (ses yok)
133
+
134
+ ⚙️ NASIL KULLANILIR:
135
+ 1. "İndirme Aracını Başlat" seçin
136
+ 2. Medya linkini girin
137
+ 3. Formatı seçin
138
+ 4. İndirme otomatik başlayacak
139
+ 5. Dosyalar 'downloads' klasöründe kaydedilir
140
+
141
+ 🔒 UYARILAR:
142
+ • Sadece yasal amaçlar için kullanın
143
+ • Telif haklarına saygı gösterin
144
+ • Başkasının içeriğini paylaşmayın
145
+
146
+ 📁 KAYITLAR:
147
+ • Tüm işlemler 'logs' klasöründe kaydedilir
148
+ • Tarih ve saat ile adlandırılmış dosyalar
149
+ """
150
+ else:
151
+ doc_text = """
152
+ ╔════════════════════════════════════════════════════════════╗
153
+ ║ 📚 SAVEHUB CLI - USER GUIDE ║
154
+ ╚════════════════════════════════════════════════════════════╝
155
+
156
+ 🎥 SUPPORTED PLATFORMS:
157
+ • YouTube - Videos and playlists
158
+ • Instagram - Photos and videos
159
+ • Facebook - Videos
160
+ • X (Twitter) - Videos and GIFs
161
+ • LinkedIn - Videos
162
+ • Snapchat - Videos
163
+
164
+ 📊 DOWNLOAD FORMATS:
165
+ • MP4 (Video+Audio) - Download with audio and video
166
+ • MP3 (Audio) - Download audio only
167
+ • Silent Video - Download video only (no audio)
168
+
169
+ ⚙️ HOW TO USE:
170
+ 1. Select "Start Download Tool"
171
+ 2. Enter media URL
172
+ 3. Select format
173
+ 4. Download will start automatically
174
+ 5. Files will be saved in 'downloads' folder
175
+
176
+ 🔒 WARNINGS:
177
+ • Use only for legal purposes
178
+ • Respect copyright rights
179
+ • Do not share others' content
180
+
181
+ 📁 LOGS:
182
+ • All operations are saved in 'logs' folder
183
+ • Files are named with date and time
184
+ """
185
+
186
+ print(doc_text)
187
+
188
+
189
+ def show_terms(lang='en'):
190
+ """Display terms of use"""
191
+ if lang == 'az':
192
+ terms_text = """
193
+ ╔════════════════════════════════════════════════════════════╗
194
+ ║ 📋 İSTİFADƏ ŞƏRTLƏRI ║
195
+ ╚════════════════════════════════════════════════════════════╝
196
+
197
+ 1. 🔒 HÜQUQI MƏSULIYYƏT
198
+ SaveHub CLI yalnız qanuni və şəxsi məqsədlər üçün
199
+ istifadə edilməlidir.
200
+
201
+ 2. 📋 PLATFORM QANUNLARI
202
+ Hər platformanın öz şərtləri vardır. Platformaların
203
+ şərtlərinə riayət edin.
204
+
205
+ 3. ⚖️ MƏSULIYYƏT REDDI
206
+ Müəlliflik hüquqları orijinal müəllifə aiddir.
207
+ SaveHub yaradıcı deyil, yardımçıdır.
208
+
209
+ 4. 🚫 MƏHDUDIYYƏTLƏR
210
+ • Kütləvi endirmə qadağandır
211
+ • Kommersial məqsədlərlə istifadə qadağandır
212
+ • Başqasının kontentini paylaşmaq qadağandır
213
+
214
+ 5. ✅ QƏBUL
215
+ Bu proqramı istifadə edərək şərtləri qəbul edirsiniz.
216
+ """
217
+ elif lang == 'tr':
218
+ terms_text = """
219
+ ╔════════════════════════════════════════════════════════════╗
220
+ ║ 📋 KULLANIM ŞARTLARI ║
221
+ ╚════════════════════════════════════════════════════════════╝
222
+
223
+ 1. 🔒 YASAL SORUMLULUK
224
+ SaveHub CLI sadece yasal ve kişisel amaçlar için
225
+ kullanılmalıdır.
226
+
227
+ 2. 📋 PLATFORM YASALARI
228
+ Her platformanın kendi şartları vardır. Platform
229
+ şartlarına uyun.
230
+
231
+ 3. ⚖️ SORUMLULUK REDDI
232
+ Telif hakları orijinal sahibine aittir.
233
+ SaveHub yaratıcı değil, kolaylaştırıcıdır.
234
+
235
+ 4. 🚫 KISITLAMALAR
236
+ • Toplu indirme yasaktır
237
+ • Ticari amaçla kullanım yasaktır
238
+ • Başkasının içeriğini paylaşmak yasaktır
239
+
240
+ 5. ✅ KABUL
241
+ Bu programı kullanarak şartları kabul edersiniz.
242
+ """
243
+ else:
244
+ terms_text = """
245
+ ╔════════════════════════════════════════════════════════════╗
246
+ ║ 📋 TERMS OF USE ║
247
+ ╚════════════════════════════════════════════════════════════╝
248
+
249
+ 1. 🔒 LEGAL RESPONSIBILITY
250
+ SaveHub CLI should only be used for legal and
251
+ personal purposes.
252
+
253
+ 2. 📋 PLATFORM LAWS
254
+ Each platform has its own terms. Comply with
255
+ platform terms.
256
+
257
+ 3. ⚖️ DISCLAIMER
258
+ Copyright rights belong to the original author.
259
+ SaveHub is a facilitator, not a creator.
260
+
261
+ 4. 🚫 RESTRICTIONS
262
+ • Bulk downloading is prohibited
263
+ • Commercial use is prohibited
264
+ • Sharing others' content is prohibited
265
+
266
+ 5. ✅ ACCEPTANCE
267
+ By using this program, you accept these terms.
268
+ """
269
+
270
+ print(terms_text)
271
+
272
+
273
+ def show_feedback(lang='en'):
274
+ """Display feedback information"""
275
+ if lang == 'az':
276
+ feedback_text = """
277
+ ╔════════════════════════════════════════════════════════════╗
278
+ ║ 💬 RƏY GÖNDƏR ║
279
+ ╚════════════════════════════════════════════════════════════╝
280
+
281
+ Sizin fikirləriniz bizə çox maraqlıdır!
282
+
283
+ 📧 ƏLAQƏ YOLLARI:
284
+
285
+ 1️⃣ GitHub Issues
286
+ https://github.com/Sadiq-Musayev/SaveHub-CLI/issues
287
+
288
+ 2️⃣ GitHub Repository
289
+ https://github.com/Sadiq-Musayev/SaveHub-CLI
290
+
291
+ 💡 MÜŞTƏRƏK OLMAQ İÇƏN:
292
+ • Xətaları bildir
293
+ • Yeni xüsusiyyətlər təklif et
294
+ • Kodda dəyişikliklər göndər
295
+ • Sənədləşməni yaxşılaşdır
296
+
297
+ Sizin dəstəyiniz üçün təşəkkürlər! ❤️
298
+ """
299
+ elif lang == 'tr':
300
+ feedback_text = """
301
+ ╔════════════════════════════════════════════════════════════╗
302
+ ║ 💬 GERİ BİLDİRİM GÖNDER ║
303
+ ╚════════════════════════════════════════════════════════════╝
304
+
305
+ Görüşleriniz bize çok önemli!
306
+
307
+ 📧 İLETİŞİM YOLLARI:
308
+
309
+ 1️⃣ GitHub Issues
310
+ https://github.com/Sadiq-Musayev/SaveHub-CLI/issues
311
+
312
+ 2️⃣ GitHub Repository
313
+ https://github.com/Sadiq-Musayev/SaveHub-CLI
314
+
315
+ 🤝 KATKIDA BULUNMAK İÇİN:
316
+ • Hataları bildir
317
+ • Yeni özellikler öner
318
+ • Kodda değişiklikler gönder
319
+ • Belgeleri iyileştir
320
+
321
+ Desteğiniz için teşekkür ederiz! ❤️
322
+ """
323
+ else:
324
+ feedback_text = """
325
+ ╔════════════════════════════════════════════════════════════╗
326
+ ║ 💬 SEND FEEDBACK ║
327
+ ╚════════════════════════════════════════════════════════════╝
328
+
329
+ Your opinions are very important to us!
330
+
331
+ 📧 CONTACT WAYS:
332
+
333
+ 1️⃣ GitHub Issues
334
+ https://github.com/Sadiq-Musayev/SaveHub-CLI/issues
335
+
336
+ 2️⃣ GitHub Repository
337
+ https://github.com/Sadiq-Musayev/SaveHub-CLI
338
+
339
+ 🤝 HOW TO CONTRIBUTE:
340
+ • Report bugs
341
+ • Suggest new features
342
+ • Submit code changes
343
+ • Improve documentation
344
+
345
+ Thank you for your support! ❤️
346
+ """
347
+
348
+ print(feedback_text)
@@ -0,0 +1,85 @@
1
+ """
2
+ SaveHub CLI - Utilities Module
3
+ """
4
+
5
+ import re
6
+ import logging
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ logging.basicConfig(
11
+ level=logging.INFO,
12
+ format='%(asctime)s - %(levelname)s - %(message)s'
13
+ )
14
+ logger = logging.getLogger(__name__)
15
+
16
+ SUPPORTED_PLATFORMS = {
17
+ 'YouTube': [r'(youtube\.com|youtu\.be)'],
18
+ 'Instagram': [r'instagram\.com'],
19
+ 'Facebook': [r'(facebook\.com|fb\.watch)'],
20
+ 'Twitter/X': [r'(twitter\.com|x\.com)'],
21
+ 'LinkedIn': [r'linkedin\.com'],
22
+ 'Snapchat': [r'snapchat\.com'],
23
+ }
24
+
25
+
26
+ def validate_url(url: str) -> bool:
27
+ """Validates whether the given string is a properly formatted URL."""
28
+ if not url or not isinstance(url, str):
29
+ return False
30
+ regex = re.compile(
31
+ r'^https?://'
32
+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|'
33
+ r'localhost|'
34
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
35
+ r'(?::\d+)?'
36
+ r'(?:/?|[/?]\S+)$',
37
+ re.IGNORECASE
38
+ )
39
+ return re.match(regex, url) is not None
40
+
41
+
42
+ def is_valid_url(url: str) -> bool:
43
+ """Alias for validate_url (backward compatibility)."""
44
+ return validate_url(url)
45
+
46
+
47
+ def get_platform_from_url(url: str) -> str | None:
48
+ """Detects which supported platform a URL belongs to."""
49
+ if not url:
50
+ return None
51
+ for platform, patterns in SUPPORTED_PLATFORMS.items():
52
+ for pattern in patterns:
53
+ if re.search(pattern, url, re.IGNORECASE):
54
+ return platform
55
+ return None
56
+
57
+
58
+ def log_event(message: str, level: str = 'INFO') -> None:
59
+ """Logs an event to console and to a dated log file."""
60
+ level = level.upper()
61
+ if level == 'ERROR':
62
+ logger.error(message)
63
+ elif level == 'WARNING':
64
+ logger.warning(message)
65
+ else:
66
+ logger.info(message)
67
+
68
+ try:
69
+ Path('logs').mkdir(exist_ok=True)
70
+ log_filename = f"logs/{datetime.now().strftime('%Y-%m-%d')}.log"
71
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
72
+ with open(log_filename, 'a', encoding='utf-8') as f:
73
+ f.write(f"[{timestamp}] [{level}] {message}\n")
74
+ except OSError as e:
75
+ logger.warning(f"Could not write to log file: {e}")
76
+
77
+
78
+ def log_message(message: str) -> None:
79
+ """Alias for log_event (backward compatibility)."""
80
+ log_event(message)
81
+
82
+
83
+ def helper_function():
84
+ """Placeholder helper function for future use."""
85
+ pass
@@ -0,0 +1,119 @@
1
+ """
2
+ SaveHub CLI - Translations Module
3
+ Supports: Azərbaycanca (AZ), English (EN), Türkçe (TR)
4
+ """
5
+
6
+ TRANSLATIONS = {
7
+ 'hello': {
8
+ 'en': 'Hello',
9
+ 'az': 'Salam',
10
+ 'tr': 'Merhaba'
11
+ },
12
+ 'goodbye': {
13
+ 'en': 'Goodbye',
14
+ 'az': 'Sağol',
15
+ 'tr': 'Hoşça kal'
16
+ },
17
+ 'thank_you': {
18
+ 'en': 'Thank you',
19
+ 'az': 'Təşəkkür edirəm',
20
+ 'tr': 'Teşekkür ederim'
21
+ },
22
+ 'press_enter': {
23
+ 'en': 'Press Enter to continue...',
24
+ 'az': 'Davam etmək üçün Enter basın...',
25
+ 'tr': 'Devam etmek için Enter\'a basın...'
26
+ },
27
+ 'see_you': {
28
+ 'en': 'See you next time!',
29
+ 'az': 'Görüşənədək!',
30
+ 'tr': 'Görüşürüz!'
31
+ },
32
+ 'main_menu_question': {
33
+ 'en': 'What would you like to do?',
34
+ 'az': 'Nə etmək istəyirsiniz?',
35
+ 'tr': 'Ne yapmak istersiniz?'
36
+ },
37
+ 'option_docs': {
38
+ 'en': '📚 Documentation',
39
+ 'az': '📚 Sənədləşmə',
40
+ 'tr': '📚 Belgeler'
41
+ },
42
+ 'option_terms': {
43
+ 'en': '📋 Terms of Use',
44
+ 'az': '📋 İstifadə Şərtləri',
45
+ 'tr': '📋 Kullanım Şartları'
46
+ },
47
+ 'option_feedback': {
48
+ 'en': '💬 Send Feedback',
49
+ 'az': '💬 Rəy Göndər',
50
+ 'tr': '💬 Geri Bildirim Gönder'
51
+ },
52
+ 'option_start': {
53
+ 'en': '▶️ Start Download Tool',
54
+ 'az': '▶️ Endirmə Alətini Başlat',
55
+ 'tr': '▶️ İndirme Aracını Başlat'
56
+ },
57
+ 'option_exit': {
58
+ 'en': '🚪 Exit',
59
+ 'az': '🚪 Çıxış',
60
+ 'tr': '🚪 Çıkış'
61
+ },
62
+ 'welcome': {
63
+ 'en': 'SaveHub Download Tool',
64
+ 'az': 'SaveHub Endirmə Aləti',
65
+ 'tr': 'SaveHub İndirme Aracı'
66
+ },
67
+ 'enter_url': {
68
+ 'en': 'Enter media URL:',
69
+ 'az': 'Media linkini daxil edin:',
70
+ 'tr': 'Medya linkini girin:'
71
+ },
72
+ 'select_format': {
73
+ 'en': 'Select download format:',
74
+ 'az': 'Endirmə formatını seçin:',
75
+ 'tr': 'İndirme formatını seçin:'
76
+ },
77
+ 'downloading': {
78
+ 'en': 'Downloading, please wait...',
79
+ 'az': 'Yüklənir, zəhmət olmasa gözləyin...',
80
+ 'tr': 'İndiriliyor, lütfen bekleyin...'
81
+ },
82
+ 'download_complete': {
83
+ 'en': 'Download complete!',
84
+ 'az': 'Yükləmə tamamlandı!',
85
+ 'tr': 'İndirme tamamlandı!'
86
+ },
87
+ 'error_download': {
88
+ 'en': 'Download error',
89
+ 'az': 'Endirmə xətası',
90
+ 'tr': 'İndirme hatası'
91
+ },
92
+ 'error_invalid_url': {
93
+ 'en': 'Please enter a valid URL',
94
+ 'az': 'Zəhmət olmasa düzgün URL daxil edin',
95
+ 'tr': 'Lütfen geçerli bir URL girin'
96
+ },
97
+ 'error_no_url': {
98
+ 'en': 'No URL entered!',
99
+ 'az': 'URL daxil edilmədi!',
100
+ 'tr': 'URL girilmedi!'
101
+ },
102
+ 'download_cancelled': {
103
+ 'en': 'Download cancelled.',
104
+ 'az': 'Yükləmə dayandırıldı.',
105
+ 'tr': 'İndirme iptal edildi.'
106
+ },
107
+ }
108
+
109
+
110
+ def get_text(key: str, lang: str = 'en') -> str:
111
+ """
112
+ Get translated text for a given key and language.
113
+ Falls back to English if language not found,
114
+ then to the key itself if completely missing.
115
+ """
116
+ entry = TRANSLATIONS.get(key)
117
+ if entry is None:
118
+ return key
119
+ return entry.get(lang) or entry.get('en') or key
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: savehub-cli
3
+ Version: 1.0.0
4
+ Summary: Multilingual CLI tool to download videos, audio and images from social media platforms
5
+ Author: Sadiq Musayev
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Sadiq-Musayev/SaveHub-CLI
8
+ Project-URL: Repository, https://github.com/Sadiq-Musayev/SaveHub-CLI
9
+ Project-URL: Issues, https://github.com/Sadiq-Musayev/SaveHub-CLI/issues
10
+ Keywords: youtube,instagram,downloader,cli,mp3,mp4,social-media
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Multimedia :: Video
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: yt-dlp>=2024.1.0
26
+ Requires-Dist: questionary>=2.0.0
27
+ Requires-Dist: colorama>=0.4.6
28
+ Requires-Dist: requests>=2.31.0
29
+ Requires-Dist: python-dotenv>=1.0.0
30
+ Dynamic: license-file
31
+
32
+ # SaveHub CLI
33
+
34
+ **Multilingual social media downloader — MP4 · MP3 · Silent Video**
35
+
36
+ > Supports: 🇦🇿 Azərbaycanca · 🇬🇧 English · 🇹🇷 Türkçe
37
+
38
+ ---
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install savehub-cli
44
+ ```
45
+
46
+ > **Requires Python 3.10+** and [FFmpeg](https://ffmpeg.org/download.html) (for MP3 conversion).
47
+
48
+ ---
49
+
50
+ ## Usage
51
+
52
+ ```bash
53
+ savehub-cli
54
+ # or
55
+ savehub
56
+ ```
57
+
58
+ 1. Select your language
59
+ 2. Choose **Start Download Tool**
60
+ 3. Paste any supported URL
61
+ 4. Select format: **MP4 · MP3 · Silent Video**
62
+ 5. Files are saved to the `downloads/` folder
63
+
64
+ ---
65
+
66
+ ## Supported Platforms
67
+
68
+ | Platform | Video | Audio | Photo |
69
+ |-------------|:-----:|:-----:|:-----:|
70
+ | YouTube | ✅ | ✅ | — |
71
+ | Instagram | ✅ | ✅ | ✅ |
72
+ | Facebook | ✅ | — | — |
73
+ | X (Twitter) | ✅ | — | — |
74
+ | LinkedIn | ✅ | — | — |
75
+ | Snapchat | ✅ | — | — |
76
+
77
+ ---
78
+
79
+ ## Download Formats
80
+
81
+ | Format | Description |
82
+ |--------------|------------------------------|
83
+ | MP4 | Video + Audio (best quality) |
84
+ | MP3 | Audio only (192kbps) |
85
+ | Silent Video | Video only, no audio |
86
+
87
+ ---
88
+
89
+ ## Controls During Download
90
+
91
+ | Key | Action |
92
+ |-----|----------------|
93
+ | `S` | Pause / Resume |
94
+ | `C` | Cancel |
95
+
96
+ ---
97
+
98
+ ## Requirements
99
+
100
+ - Python ≥ 3.10
101
+ - FFmpeg (for MP3 export) — [Download here](https://ffmpeg.org/download.html)
102
+ - Dependencies: `yt-dlp`, `questionary`, `colorama`, `requests`, `python-dotenv`
103
+
104
+ ---
105
+
106
+ ## Logs
107
+
108
+ All download events are saved in `logs/YYYY-MM-DD.log`.
109
+
110
+ ---
111
+
112
+ ## Contributing
113
+
114
+ 1. Fork the repo
115
+ 2. Create your branch: `git checkout -b feature/my-feature`
116
+ 3. Commit: `git commit -m "Add my feature"`
117
+ 4. Push: `git push origin feature/my-feature`
118
+ 5. Open a Pull Request
119
+
120
+ Bug reports and feature requests → [GitHub Issues](https://github.com/Sadiq-Musayev/SaveHub-CLI/issues)
121
+
122
+ ---
123
+
124
+ ## License
125
+
126
+ [MIT](LICENSE) © 2025 Sadiq Musayev
@@ -0,0 +1,14 @@
1
+ savehub_cli/__init__.py,sha256=RS7zal1RgLE87V9s2XmAO5R-KMln9MF_YcMma6EKgKs,195
2
+ savehub_cli/config.py,sha256=Q1qw0oHC-VUS9X3OH8nhzyiXSAgKb0_dHxzKfFE1Rts,950
3
+ savehub_cli/main.py,sha256=SgER40JruwoVe-8yJ-s86bn8D1fpBtnSCyofEOESUsc,2485
4
+ savehub_cli/translations.py,sha256=KjJLv_vX-2-0XZ3kY4X28dzGT4nLMsMLgKfQ855wlJ4,3469
5
+ savehub_cli/modules/__init__.py,sha256=OCgLPqYyE0OFm2IKDQUMZ7JIxAAbOI6uWk-fb9czKJQ,208
6
+ savehub_cli/modules/downloader.py,sha256=iSI9oEdOY5IwT-vtDH_ink3XMy3FGJu6mr4nZIpxRdg,15079
7
+ savehub_cli/modules/menu.py,sha256=c9dmfYHUdTPw3t-TWCpbYgvjfIHCPp8zdIb1FCsLDuk,14113
8
+ savehub_cli/modules/utils.py,sha256=OvNS1omdtOJH4pBs3Xm7Etn4hYnZ5h4dMX5PkFiJ7Vg,2494
9
+ savehub_cli-1.0.0.dist-info/licenses/LICENSE,sha256=CpCEkJEQYbnyJs8h4fBHS6n-OIR4HoIthkpZ9CZqqrM,1070
10
+ savehub_cli-1.0.0.dist-info/METADATA,sha256=JWYhKPoYUo1EEQvii436ucP62Pub7sQ9JMiGBxzdCHo,3351
11
+ savehub_cli-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ savehub_cli-1.0.0.dist-info/entry_points.txt,sha256=TuiuWSzWYtMEHhZhGfsE4LCEIFIhhUae_q3em2urcCQ,86
13
+ savehub_cli-1.0.0.dist-info/top_level.txt,sha256=quzfCkswKTGU8QYhcprTD6ra2a61FdUd38JwaCYfcDk,12
14
+ savehub_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ savehub = savehub_cli.main:main
3
+ savehub-cli = savehub_cli.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sadiq Musayev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ savehub_cli