weeb-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,47 @@
1
+ import subprocess
2
+ import shutil
3
+ import sys
4
+ from rich.console import Console
5
+
6
+ from weeb_cli.services.dependency_manager import dependency_manager
7
+ from weeb_cli.i18n import i18n
8
+
9
+ console = Console()
10
+
11
+ class Player:
12
+ def __init__(self):
13
+ self.mpv_path = dependency_manager.check_dependency("mpv")
14
+
15
+ def is_installed(self):
16
+ return self.mpv_path is not None
17
+
18
+ def play(self, url, title=None, start_time=None, headers=None):
19
+ if not self.mpv_path:
20
+ console.print(f"[yellow]{i18n.get('player.installing_mpv')}[/yellow]")
21
+ if dependency_manager.install_dependency("mpv"):
22
+ self.mpv_path = dependency_manager.check_dependency("mpv")
23
+
24
+ if not self.mpv_path:
25
+ console.print(f"[red]{i18n.get('player.install_failed')}[/red]")
26
+ return False
27
+
28
+ cmd = [self.mpv_path, url]
29
+ if title:
30
+ cmd.extend([f"--force-media-title={title}"])
31
+
32
+ if headers:
33
+ header_strs = [f"{k}: {v}" for k, v in headers.items()]
34
+ cmd.append(f"--http-header-fields={','.join(header_strs)}")
35
+
36
+ cmd.append("--fs")
37
+
38
+ cmd.append("--save-position-on-quit")
39
+
40
+ try:
41
+ subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
42
+ return True
43
+ except Exception as e:
44
+ console.print(f"[red]Error running player: {e}[/red]")
45
+ return False
46
+
47
+ player = Player()
@@ -0,0 +1,136 @@
1
+ import json
2
+ from pathlib import Path
3
+ from datetime import datetime
4
+
5
+ class ProgressTracker:
6
+ def __init__(self):
7
+ self.config_dir = Path.home() / ".weeb-cli"
8
+ self.progress_file = self.config_dir / "progress.json"
9
+ self.history_file = self.config_dir / "search_history.json"
10
+ self._ensure_file()
11
+
12
+ def _ensure_file(self):
13
+ self.config_dir.mkdir(parents=True, exist_ok=True)
14
+ if not self.progress_file.exists():
15
+ with open(self.progress_file, 'w') as f:
16
+ json.dump({}, f)
17
+ if not self.history_file.exists():
18
+ with open(self.history_file, 'w') as f:
19
+ json.dump([], f)
20
+
21
+ def load_progress(self):
22
+ try:
23
+ with open(self.progress_file, 'r', encoding='utf-8') as f:
24
+ return json.load(f)
25
+ except (json.JSONDecodeError, FileNotFoundError):
26
+ return {}
27
+
28
+ def save_progress(self, data):
29
+ with open(self.progress_file, 'w', encoding='utf-8') as f:
30
+ json.dump(data, f, indent=2, ensure_ascii=False)
31
+
32
+ def get_anime_progress(self, slug):
33
+ data = self.load_progress()
34
+ return data.get(slug, {
35
+ "last_watched": 0,
36
+ "completed": [],
37
+ "title": "",
38
+ "total_episodes": 0,
39
+ "last_watched_at": None
40
+ })
41
+
42
+ def mark_watched(self, slug, ep_number, title=None, total_episodes=None):
43
+ data = self.load_progress()
44
+ if slug not in data:
45
+ data[slug] = {
46
+ "last_watched": 0,
47
+ "completed": [],
48
+ "title": title or slug,
49
+ "total_episodes": total_episodes or 0,
50
+ "last_watched_at": None
51
+ }
52
+
53
+ completed_set = set(data[slug].get("completed", []))
54
+ completed_set.add(ep_number)
55
+ data[slug]["completed"] = sorted(list(completed_set))
56
+
57
+ if ep_number > data[slug].get("last_watched", 0):
58
+ data[slug]["last_watched"] = ep_number
59
+
60
+ data[slug]["last_watched_at"] = datetime.now().isoformat()
61
+
62
+ if title:
63
+ data[slug]["title"] = title
64
+ if total_episodes:
65
+ data[slug]["total_episodes"] = total_episodes
66
+
67
+ self.save_progress(data)
68
+
69
+ def get_all_anime(self):
70
+ return self.load_progress()
71
+
72
+ def get_stats(self):
73
+ data = self.load_progress()
74
+ total_anime = len(data)
75
+ total_episodes = sum(len(a.get("completed", [])) for a in data.values())
76
+ total_hours = round(total_episodes * 24 / 60, 1)
77
+
78
+ last_watched = None
79
+ last_time = None
80
+ for slug, info in data.items():
81
+ watched_at = info.get("last_watched_at")
82
+ if watched_at:
83
+ if last_time is None or watched_at > last_time:
84
+ last_time = watched_at
85
+ last_watched = {"slug": slug, **info}
86
+
87
+ return {
88
+ "total_anime": total_anime,
89
+ "total_episodes": total_episodes,
90
+ "total_hours": total_hours,
91
+ "last_watched": last_watched
92
+ }
93
+
94
+ def get_completed_anime(self):
95
+ data = self.load_progress()
96
+ completed = []
97
+ for slug, info in data.items():
98
+ total = info.get("total_episodes", 0)
99
+ watched = len(info.get("completed", []))
100
+ if total > 0 and watched >= total:
101
+ completed.append({"slug": slug, **info})
102
+ return completed
103
+
104
+ def get_in_progress_anime(self):
105
+ data = self.load_progress()
106
+ in_progress = []
107
+ for slug, info in data.items():
108
+ total = info.get("total_episodes", 0)
109
+ watched = len(info.get("completed", []))
110
+ if watched > 0 and (total == 0 or watched < total):
111
+ in_progress.append({"slug": slug, **info})
112
+ return sorted(in_progress, key=lambda x: x.get("last_watched_at") or "", reverse=True)
113
+
114
+ def add_search_history(self, query):
115
+ try:
116
+ with open(self.history_file, 'r', encoding='utf-8') as f:
117
+ history = json.load(f)
118
+ except:
119
+ history = []
120
+
121
+ if query in history:
122
+ history.remove(query)
123
+ history.insert(0, query)
124
+ history = history[:10]
125
+
126
+ with open(self.history_file, 'w', encoding='utf-8') as f:
127
+ json.dump(history, f, ensure_ascii=False)
128
+
129
+ def get_search_history(self):
130
+ try:
131
+ with open(self.history_file, 'r', encoding='utf-8') as f:
132
+ return json.load(f)
133
+ except:
134
+ return []
135
+
136
+ progress_tracker = ProgressTracker()
@@ -0,0 +1,91 @@
1
+ from typing import List, Optional, Tuple
2
+ from weeb_cli.config import config
3
+ from weeb_cli.providers import get_provider, get_default_provider, list_providers
4
+ from weeb_cli.providers.base import AnimeResult, AnimeDetails, Episode, StreamLink, ProviderError
5
+
6
+
7
+ class Scraper:
8
+
9
+ def __init__(self):
10
+ self._provider = None
11
+ self._provider_name = None
12
+ self.last_error = None
13
+
14
+ @property
15
+ def provider(self):
16
+ current_source = config.get("scraping_source", "")
17
+
18
+ if self._provider_name != current_source:
19
+ self._provider_name = current_source
20
+ self._provider = get_provider(current_source)
21
+
22
+ if not self._provider:
23
+ lang = config.get("language", "tr")
24
+ default = get_default_provider(lang)
25
+ if default:
26
+ self._provider = get_provider(default)
27
+ self._provider_name = default
28
+
29
+ return self._provider
30
+
31
+ def search(self, query: str) -> List[AnimeResult]:
32
+ self.last_error = None
33
+ if not self.provider:
34
+ return []
35
+ try:
36
+ return self.provider.search(query)
37
+ except ProviderError as e:
38
+ self.last_error = e.code
39
+ return []
40
+ except Exception as e:
41
+ self.last_error = str(e)
42
+ return []
43
+
44
+ def get_details(self, anime_id: str) -> Optional[AnimeDetails]:
45
+ self.last_error = None
46
+ if not self.provider:
47
+ return None
48
+ try:
49
+ return self.provider.get_details(anime_id)
50
+ except ProviderError as e:
51
+ self.last_error = e.code
52
+ return None
53
+ except Exception as e:
54
+ self.last_error = str(e)
55
+ return None
56
+
57
+ def get_episodes(self, anime_id: str) -> List[Episode]:
58
+ self.last_error = None
59
+ if not self.provider:
60
+ return []
61
+ try:
62
+ return self.provider.get_episodes(anime_id)
63
+ except ProviderError as e:
64
+ self.last_error = e.code
65
+ return []
66
+ except Exception as e:
67
+ self.last_error = str(e)
68
+ return []
69
+
70
+ def get_streams(self, anime_id: str, episode_id: str) -> List[StreamLink]:
71
+ self.last_error = None
72
+ if not self.provider:
73
+ return []
74
+ try:
75
+ return self.provider.get_streams(anime_id, episode_id)
76
+ except ProviderError as e:
77
+ self.last_error = e.code
78
+ return []
79
+ except Exception as e:
80
+ self.last_error = str(e)
81
+ return []
82
+
83
+ def get_available_sources(self) -> List[dict]:
84
+ return list_providers()
85
+
86
+ def get_sources_for_lang(self, lang: str) -> List[str]:
87
+ from weeb_cli.providers import get_providers_for_lang
88
+ return get_providers_for_lang(lang)
89
+
90
+
91
+ scraper = Scraper()
@@ -0,0 +1,16 @@
1
+ from weeb_cli.services.scraper import scraper
2
+
3
+ def search(query):
4
+ results = scraper.search(query)
5
+ return [
6
+ {
7
+ "id": r.id,
8
+ "title": r.title,
9
+ "name": r.title,
10
+ "slug": r.id,
11
+ "type": r.type,
12
+ "cover": r.cover,
13
+ "year": r.year
14
+ }
15
+ for r in results
16
+ ]
@@ -0,0 +1,199 @@
1
+ import requests
2
+ from packaging import version
3
+ from weeb_cli import __version__
4
+ from rich.console import Console
5
+ import questionary
6
+ from weeb_cli.i18n import i18n
7
+ import sys
8
+ import os
9
+ import platform
10
+ import subprocess
11
+ import tempfile
12
+
13
+ console = Console()
14
+
15
+ def get_install_method():
16
+ if getattr(sys, 'frozen', False):
17
+ return "exe"
18
+
19
+ try:
20
+ result = subprocess.run(
21
+ [sys.executable, "-m", "pip", "show", "weeb-cli"],
22
+ capture_output=True, text=True, timeout=5
23
+ )
24
+ if result.returncode == 0:
25
+ return "pip"
26
+ except Exception:
27
+ pass
28
+
29
+ return "standalone"
30
+
31
+ def get_platform_info():
32
+ system = platform.system().lower()
33
+ machine = platform.machine().lower()
34
+
35
+ if system == "windows":
36
+ return "windows", "exe"
37
+ elif system == "darwin":
38
+ return "macos", "macos" if "arm" in machine else "macos-intel"
39
+ elif system == "linux":
40
+ return "linux", "linux"
41
+ else:
42
+ return system, system
43
+
44
+ def check_for_updates():
45
+ try:
46
+ url = "https://api.github.com/repos/ewgsta/weeb-cli/releases/latest"
47
+ response = requests.get(url, timeout=5)
48
+ if response.status_code == 200:
49
+ data = response.json()
50
+ latest_tag = data.get("tag_name", "").lstrip("v")
51
+ assets = data.get("assets", [])
52
+
53
+ if not latest_tag:
54
+ return False, None, None
55
+
56
+ current_ver = version.parse(__version__)
57
+ latest_ver = version.parse(latest_tag)
58
+
59
+ if latest_ver > current_ver:
60
+ return True, latest_tag, assets
61
+
62
+ except Exception:
63
+ pass
64
+
65
+ return False, None, None
66
+
67
+ def find_asset_for_platform(assets):
68
+ system, platform_key = get_platform_info()
69
+
70
+ for asset in assets:
71
+ name = asset.get("name", "").lower()
72
+ download_url = asset.get("browser_download_url", "")
73
+
74
+ if system == "windows" and name.endswith(".exe"):
75
+ return download_url, name
76
+ elif system == "darwin" and ("macos" in name or "darwin" in name):
77
+ return download_url, name
78
+ elif system == "linux" and "linux" in name:
79
+ return download_url, name
80
+
81
+ return None, None
82
+
83
+ def download_exe(url, filename):
84
+ try:
85
+ console.print(f"[cyan]{i18n.get('update.downloading')}...[/cyan]")
86
+
87
+ response = requests.get(url, stream=True, timeout=60)
88
+ response.raise_for_status()
89
+
90
+ total_size = int(response.headers.get('content-length', 0))
91
+
92
+ if getattr(sys, 'frozen', False):
93
+ current_exe = sys.executable
94
+ download_dir = os.path.dirname(current_exe)
95
+ new_exe_path = os.path.join(download_dir, f"weeb-cli-new.exe")
96
+ else:
97
+ download_dir = tempfile.gettempdir()
98
+ new_exe_path = os.path.join(download_dir, filename)
99
+
100
+ downloaded = 0
101
+ with open(new_exe_path, 'wb') as f:
102
+ for chunk in response.iter_content(chunk_size=8192):
103
+ if chunk:
104
+ f.write(chunk)
105
+ downloaded += len(chunk)
106
+ if total_size > 0:
107
+ percent = int((downloaded / total_size) * 100)
108
+ console.print(f"\r[cyan]İndiriliyor: {percent}%[/cyan]", end="")
109
+
110
+ console.print(f"\n[green]{i18n.get('update.downloaded')}[/green]")
111
+ console.print(f"[dim]Konum: {new_exe_path}[/dim]")
112
+
113
+ if getattr(sys, 'frozen', False):
114
+ batch_content = f'''@echo off
115
+ timeout /t 2 /nobreak >nul
116
+ del "{current_exe}"
117
+ move "{new_exe_path}" "{current_exe}"
118
+ start "" "{current_exe}"
119
+ del "%~f0"
120
+ '''
121
+ batch_path = os.path.join(download_dir, "update.bat")
122
+ with open(batch_path, 'w') as f:
123
+ f.write(batch_content)
124
+
125
+ console.print(f"[green]{i18n.get('update.restarting')}...[/green]")
126
+ subprocess.Popen(['cmd', '/c', batch_path],
127
+ creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0)
128
+ sys.exit(0)
129
+
130
+ return True
131
+
132
+ except Exception as e:
133
+ console.print(f"[red]{i18n.get('update.error')}: {e}[/red]")
134
+ return False
135
+
136
+ def update_via_pip():
137
+ try:
138
+ console.print(f"[cyan]{i18n.get('update.updating_pip')}...[/cyan]")
139
+
140
+ result = subprocess.run(
141
+ [sys.executable, "-m", "pip", "install", "--upgrade", "weeb-cli"],
142
+ capture_output=True, text=True, timeout=120
143
+ )
144
+
145
+ if result.returncode == 0:
146
+ console.print(f"[green]{i18n.get('update.success')}[/green]")
147
+ console.print(f"[dim]{i18n.get('update.restart_required')}[/dim]")
148
+ return True
149
+ else:
150
+ console.print(f"[red]{i18n.get('update.error')}[/red]")
151
+ console.print(f"[dim]{result.stderr}[/dim]")
152
+ return False
153
+
154
+ except Exception as e:
155
+ console.print(f"[red]{i18n.get('update.error')}: {e}[/red]")
156
+ return False
157
+
158
+ def update_prompt():
159
+ is_available, latest_ver, assets = check_for_updates()
160
+
161
+ if not is_available:
162
+ return
163
+
164
+ console.clear()
165
+ console.print(f"\n[green bold]{i18n.get('update.available')} (v{latest_ver})[/green bold]")
166
+ console.print(f"[dim]{i18n.get('update.current')}: v{__version__}[/dim]\n")
167
+
168
+ should_update = questionary.confirm(
169
+ i18n.get("update.prompt"),
170
+ default=True
171
+ ).ask()
172
+
173
+ if not should_update:
174
+ return
175
+
176
+ install_method = get_install_method()
177
+
178
+ if install_method == "exe":
179
+ asset_url, asset_name = find_asset_for_platform(assets or [])
180
+ if asset_url:
181
+ download_exe(asset_url, asset_name)
182
+ else:
183
+ console.print(f"[red]{i18n.get('update.no_asset')}[/red]")
184
+
185
+ elif install_method == "pip":
186
+ update_via_pip()
187
+
188
+ else:
189
+ asset_url, asset_name = find_asset_for_platform(assets or [])
190
+ if asset_url:
191
+ system, _ = get_platform_info()
192
+ if system == "windows":
193
+ download_exe(asset_url, asset_name)
194
+ else:
195
+ console.print(f"[cyan]{i18n.get('update.download_url')}:[/cyan]")
196
+ console.print(f"[blue]{asset_url}[/blue]")
197
+ else:
198
+ console.print(f"[yellow]{i18n.get('update.manual_update')}[/yellow]")
199
+ console.print("[blue]pip install --upgrade weeb-cli[/blue]")
@@ -0,0 +1,19 @@
1
+ from weeb_cli.services.scraper import scraper
2
+
3
+ def get_streams(anime_id, episode_id):
4
+ streams = scraper.get_streams(anime_id, episode_id)
5
+ if not streams:
6
+ return None
7
+
8
+ return {
9
+ "data": {
10
+ "links": [
11
+ {
12
+ "url": s.url,
13
+ "quality": s.quality,
14
+ "server": s.server
15
+ }
16
+ for s in streams
17
+ ]
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ # Init UI package
weeb_cli/ui/header.py ADDED
@@ -0,0 +1,30 @@
1
+ from rich import print
2
+ from rich.text import Text
3
+ from rich.console import Console
4
+ from weeb_cli.config import config
5
+ from weeb_cli import __version__
6
+
7
+ console = Console()
8
+
9
+ def show_header(title="Weeb API", show_version=False, show_source=False):
10
+ console.clear()
11
+
12
+ text = Text()
13
+ text.append(f" {title} ", style="bold white on blue")
14
+
15
+ parts = []
16
+
17
+ if show_source:
18
+ cfg_source = config.get("scraping_source", "local")
19
+ disp_source = "weeb" if cfg_source == "local" else cfg_source
20
+ parts.append(disp_source)
21
+
22
+ if show_version:
23
+ parts.append(f"v{__version__}")
24
+
25
+ if parts:
26
+ joined = " | ".join(parts)
27
+ text.append(f" | {joined}", style="dim white")
28
+
29
+ console.print(text, justify="left")
30
+ print()
weeb_cli/ui/menu.py ADDED
@@ -0,0 +1,59 @@
1
+ import questionary
2
+ from rich.console import Console
3
+ import sys
4
+ from .header import show_header
5
+ from weeb_cli.i18n import i18n
6
+ from weeb_cli.commands.search import search_anime
7
+ from weeb_cli.commands.settings import open_settings
8
+ from weeb_cli.commands.watchlist import show_watchlist
9
+ from weeb_cli.commands.downloads import show_downloads
10
+
11
+ console = Console()
12
+
13
+ def show_main_menu():
14
+ console.clear()
15
+ show_header("Weeb API", show_version=True, show_source=True)
16
+
17
+ opt_search = i18n.get("menu.options.search")
18
+ opt_watchlist = i18n.get("menu.options.watchlist")
19
+ opt_downloads = i18n.get("menu.options.downloads")
20
+ opt_settings = i18n.get("menu.options.settings")
21
+ opt_exit = i18n.get("menu.options.exit")
22
+
23
+ try:
24
+ selected = questionary.select(
25
+ i18n.get("menu.prompt"),
26
+ choices=[
27
+ opt_search,
28
+ opt_watchlist,
29
+ opt_downloads,
30
+ opt_settings,
31
+ opt_exit
32
+ ],
33
+ pointer=">",
34
+ use_shortcuts=False,
35
+ style=questionary.Style([
36
+ ('pointer', 'fg:cyan bold'),
37
+ ('highlighted', 'fg:cyan'),
38
+ ('selected', 'fg:cyan bold'),
39
+ ])
40
+ ).ask()
41
+
42
+ console.clear()
43
+
44
+ if selected == opt_search:
45
+ search_anime()
46
+ elif selected == opt_watchlist:
47
+ show_watchlist()
48
+ elif selected == opt_downloads:
49
+ show_downloads()
50
+ elif selected == opt_settings:
51
+ open_settings()
52
+ elif selected == opt_exit or selected is None:
53
+ console.print(f"[yellow] {i18n.get('common.success')}...[/yellow]")
54
+ sys.exit(0)
55
+
56
+ show_main_menu()
57
+
58
+ except KeyboardInterrupt:
59
+ sys.exit(0)