diff --git a/weeb_cli/commands/search/watch_flow.py b/weeb_cli/commands/search/watch_flow.py index 3e21468..9cbb391 100644 --- a/weeb_cli/commands/search/watch_flow.py +++ b/weeb_cli/commands/search/watch_flow.py @@ -171,13 +171,17 @@ def _play_episode(slug, selected_ep, details, season, episodes, completed_ids): return False from weeb_cli.services.stream_validator import stream_validator + from weeb_cli.config import config - console.print(f"[dim]{i18n.t('details.validating_streams')}...[/dim]") valid_streams = [] - for stream in streams_list: - is_valid, error = stream_validator.validate_url(stream.get("url"), timeout=3) - if is_valid: - valid_streams.append(stream) + if config.get("scraping_source") == "docchi": + valid_streams = streams_list + else: + console.print(f"[dim]{i18n.t('details.validating_streams')}...[/dim]") + for stream in streams_list: + is_valid, error = stream_validator.validate_url(stream.get("url"), timeout=3) + if is_valid: + valid_streams.append(stream) if not valid_streams: console.print(f"[red]{i18n.t('details.no_valid_streams')}[/red]") diff --git a/weeb_cli/commands/settings/settings_config.py b/weeb_cli/commands/settings/settings_config.py index e206e05..31d9607 100644 --- a/weeb_cli/commands/settings/settings_config.py +++ b/weeb_cli/commands/settings/settings_config.py @@ -41,10 +41,10 @@ def toggle_shortcuts(): def change_language(): from weeb_cli.services.scraper import scraper - langs = {"Türkçe": "tr", "English": "en", "Deutsch": "de"} + langs = {"Türkçe": "tr", "English": "en", "Deutsch": "de", "Polski": "pl"} try: selected = questionary.select( - "Select Language / Dil Seçiniz:", + "", choices=list(langs.keys()), pointer=">", use_shortcuts=False diff --git a/weeb_cli/commands/settings/settings_menu.py b/weeb_cli/commands/settings/settings_menu.py index 7e05cb2..46e61b0 100644 --- a/weeb_cli/commands/settings/settings_menu.py +++ b/weeb_cli/commands/settings/settings_menu.py @@ -79,7 +79,7 @@ def _build_settings_menu(): choices.extend([ i18n.t("settings.trackers"), - i18n.t("settings.cache"), + i18n.t("settings.cache_title"), i18n.t("settings.backup_restore") ]) @@ -91,7 +91,7 @@ def _handle_settings_action(answer): i18n.t("settings.download_settings"): download_settings_menu, i18n.t("settings.external_drives"): external_drives_menu, i18n.t("settings.trackers"): trackers_menu, - i18n.t("settings.cache"): cache_settings_menu, + i18n.t("settings.cache_title"): cache_settings_menu, i18n.t("settings.backup_restore"): backup_restore_menu, } diff --git a/weeb_cli/config.py b/weeb_cli/config.py index 269c6ae..d940cec 100644 --- a/weeb_cli/config.py +++ b/weeb_cli/config.py @@ -4,18 +4,21 @@ APP_NAME = "weeb-cli" CONFIG_DIR = Path.home() / f".{APP_NAME}" + def get_default_download_dir(): from weeb_cli.i18n import i18n + folder_name = i18n.t("downloads.default_folder_name", "weeb-downloads") return os.path.join(os.getcwd(), folder_name) + DEFAULT_CONFIG = { - "language": None, + "language": None, "aria2_enabled": True, "ytdlp_enabled": True, "aria2_max_connections": 16, "max_concurrent_downloads": 3, - "download_dir": None, + "download_dir": None, "ytdlp_format": "bestvideo+bestaudio/best", "scraping_source": None, "show_description": True, @@ -23,21 +26,23 @@ def get_default_download_dir(): "download_max_retries": 3, "download_retry_delay": 10, "discord_rpc_enabled": True, - "shortcuts_enabled": False + "shortcuts_enabled": False, } + class Config: def __init__(self): self._db = None self._headless = False - + @property def db(self): if self._db is None: from weeb_cli.services.database import db + self._db = db return self._db - + def get(self, key, default=None): if not self._headless: try: @@ -46,20 +51,21 @@ def get(self, key, default=None): return val except Exception: pass - + # Special handling for download_dir if key == "download_dir": return default if default is not None else get_default_download_dir() - + # Use provided default, fallback to DEFAULT_CONFIG, then None if default is not None: return DEFAULT_CONFIG.get(key, default) return DEFAULT_CONFIG.get(key) - + def set(self, key, value): self.db.set_config(key, value) def set_headless(self, headless: bool = True): self._headless = headless + config = Config() diff --git a/weeb_cli/locales/de.json b/weeb_cli/locales/de.json index d10d422..160a3e8 100644 --- a/weeb_cli/locales/de.json +++ b/weeb_cli/locales/de.json @@ -150,7 +150,7 @@ "restore_confirm": "Die aktuelle Datenbank wird überschrieben. Sind Sie sicher?", "restore_success": "Wiederherstellung erfolgreich!", "restore_failed": "Wiederherstellung fehlgeschlagen.", - "cache": "Cache-Verwaltung", + "cache_title": "Cache-Verwaltung", "cache_title": "Cache-Verwaltung", "cache_memory_entries": "Speichereinträge", "cache_file_entries": "Dateieinträge", diff --git a/weeb_cli/locales/pl.json b/weeb_cli/locales/pl.json new file mode 100644 index 0000000..57e3bf9 --- /dev/null +++ b/weeb_cli/locales/pl.json @@ -0,0 +1,378 @@ +{ + "update": { + "available": "Dostępna nowa aktualizacja!", + "current": "Obecna wersja", + "prompt": "Czy chcesz zaktualizować teraz?", + "opening": "Otwieranie strony pobierania...", + "error": "Aktualizacja nie powiodła się", + "success": "Aktualizacja zakończona sukcesem!", + "restart_required": "Uruchom ponownie aplikację, aby zastosować zmiany.", + "manual_required": "Automatyczna aktualizacja niedostępna, otwieranie strony pobierania...", + "detected": "Wykryto metodę instalacji", + "running": "Uruchomione", + "timeout": "Operacja przekroczyła limit czasu.", + "fallback": "Aktualizacja nie powiodła się, otwieranie strony pobierania...", + "downloading_progress": "Pobieranie: {percent}%", + "downloaded": "Pobrano", + "location": "Lokalizacja", + "restarting": "Ponowne uruchamianie", + "updating_pip": "Aktualizacja przez pip", + "no_asset": "Nie znaleziono pliku do pobrania", + "download_url": "URL pobierania", + "manual_update": "Wymagana ręczna aktualizacja", + "pip_command": "pip install --upgrade weeb-cli" + }, + "menu": { + "title": "Menu Główne", + "prompt": "Co chcesz zrobić?", + "exit_confirm_downloads": "Istnieją aktywne pobierania. Czy na pewno chcesz wyjść?", + "options": { + "search": "Szukaj Anime", + "watchlist": "Moja Lista", + "downloads": "Pobrane", + "library": "Moja Biblioteka", + "settings": "Ustawienia", + "exit": "Wyjście" + } + }, + "settings": { + "title": "Ustawienia", + "trackers": "Trackery", + "language": "Zmień Język", + "source": "Wybierz Źródło", + "aria2": "Pobieranie Aria2", + "ytdlp": "Wsparcie yt-dlp", + "show_description": "Pokaż Opis", + "discord_rpc": "Discord RPC", + "aria2_config": "Ustawienia Aria2", + "ytdlp_config": "Ustawienia yt-dlp", + "max_conn": "Maks. Połączeń", + "download_dir": "Katalog Pobierania", + "format": "Format", + "enter_conn": "Połączenia (1-16)", + "enter_path": "Ścieżka Pobierania", + "enter_format": "Ciąg formatujący", + "download_settings": "Ustawienia Pobierania", + "change_folder_name": "Zmień Nazwę Folderu", + "change_full_path": "Zmień Pełną Ścieżkę", + "folder_name_prompt": "Nazwa Folderu:", + "full_path_prompt": "Pełna Ścieżka:", + "concurrent_downloads": "Jednoczesne Pobierania", + "enter_concurrent": "Wprowadź liczbę jednoczesnych pobrań (1-5)", + "max_retries": "Liczba Ponowień", + "retry_delay": "Opóźnienie Ponowienia (s)", + "enter_max_retries": "Wprowadź liczbę ponowień (0-10)", + "enter_retry_delay": "Wprowadź opóźnienie w sekundach", + "language_changed": "Język zmieniony na Polski.", + "source_changed": "Źródło zmienione na {source}.", + "no_sources": "Brak dostępnych źródeł dla tego języka.", + "toggle_on": "{tool} włączone.", + "toggle_off": "{tool} wyłączone.", + "external_drives": "Dyski Zewnętrzne", + "add_drive": "Dodaj Dysk", + "enter_drive_path": "Wprowadź ścieżkę dysku (np. D:\\Anime)", + "enter_drive_name": "Pseudonim dla dysku", + "drive_not_found": "Nie znaleziono podanej ścieżki.", + "drive_added": "Dysk dodany.", + "rename_drive": "Zmień Nazwę", + "remove_drive": "Usuń Dysk", + "confirm_remove": "Czy na pewno chcesz usunąć ten dysk?", + "drive_renamed": "Nazwa dysku zmieniona.", + "drive_removed": "Dysk usunięty.", + "current_dir": "Obecny: {dir}", + "retry_delay_error": "Opóźnienie ponowienia musi wynosić od 0 do 300 sekund.", + "cache": { + "title": "Zarządzanie Cache", + "memory_entries": "Wpisy w pamięci", + "file_entries": "Wpisy w plikach", + "total_size": "Całkowity rozmiar", + "clear_all": "Wyczyść cały cache", + "clear_provider": "Wyczyść cache obecnego dostawcy", + "cleanup_old": "Wyczyść stary cache (>24h)", + "cleared": "Cache wyczyszczony", + "provider_cleared": "Cache dostawcy wyczyszczony", + "cleaned": "Oczyszczono", + "prompt": "Wybierz działanie", + "confirm_clear_all": "Wyczyścić cały cache?" + }, + "anilist": "AniList", + "anilist_connected": "Połączono: {user}", + "anilist_not_connected": "Połącz konto AniList, aby synchronizować historię oglądania.", + "anilist_login": "Połącz con AniList", + "anilist_logout": "Odłącz", + "anilist_sync": "Synchronizuj Oczekujące Aktualizacje", + "anilist_pending": "{count} oczekujących aktualizacji", + "anilist_synced": "{count} zaktualizowanych wpisów", + "anilist_opening_browser": "Otwieranie przeglądarki...", + "anilist_waiting": "Zaloguj się w przeglądarce, zostanie to wykryte automatycznie...", + "anilist_timeout": "Przekroczono limit czasu. Spróbuj ponownie.", + "anilist_paste_token": "Po zalogowaniu wklej token z adresu URL tutaj:", + "anilist_login_success": "Pomyślnie połączono z AniList!", + "anilist_login_failed": "Połączenie nieudane. Token może być nieprawidłowy.", + "anilist_logged_out": "Odłączono od AniList.", + "confirm_logout": "Czy na pewno chcesz się odłączyć?", + "mal": "MyAnimeList", + "mal_connected": "Połączono: {user}", + "mal_not_connected": "Połącz konto MAL, aby synchronizować historię oglądania.", + "mal_login": "Połącz z MAL", + "mal_logout": "Odłącz", + "mal_sync": "Synchronizuj Oczekujące Aktualizacje", + "mal_pending": "{count} oczekujących aktualizacji", + "mal_synced": "{count} zaktualizowanych wpisów", + "mal_opening_browser": "Otwieranie przeglądarki...", + "mal_waiting": "Zaloguj się w przeglądarce, zostanie to wykryte automatycznie...", + "mal_login_success": "Pomyślnie połączono z MAL!", + "mal_login_failed": "Połączenie nieudane. Spróbuj ponownie.", + "mal_logged_out": "Odłączono od MAL.", + "kitsu": "Kitsu", + "kitsu_connected": "Połączono: {user}", + "kitsu_not_connected": "Połącz konto Kitsu, aby synchronizować historię oglądania.", + "kitsu_login": "Połącz z Kitsu", + "kitsu_logout": "Odłącz", + "kitsu_sync": "Synchronizuj Oczekujące Aktualizacje", + "kitsu_pending": "{count} oczekujących aktualizacji", + "kitsu_synced": "{count} zaktualizowanych wpisów", + "kitsu_enter_credentials": "Wprowadź dane logowania Kitsu", + "kitsu_email": "Email lub Nazwa użytkownika:", + "kitsu_password": "Hasło:", + "kitsu_login_success": "Pomyślnie połączono z Kitsu!", + "kitsu_login_failed": "Połączenie nieudane. Sprawdź swoje dane.", + "kitsu_logged_out": "Odłączono od Kitsu.", + "backup_restore": "Kopia Zapasowa i Przywracanie", + "db_location": "Lokalizacja Bazy Danych", + "db_size": "Rozmiar", + "create_backup": "Utwórz Kopię Zapasową", + "restore_backup": "Przywróć Kopię Zapasową", + "backup_path": "Nazwa pliku lub ścieżka kopii", + "restore_path": "Ścieżka pliku kopii", + "backup_success": "Kopia zapasowa utworzona pomyślnie!", + "backup_failed": "Tworzenie kopii zapasowej nie powiodło się.", + "restore_confirm": "Obecna baza danych zostanie nadpisana. Czy jesteś pewien?", + "restore_success": "Przywracanie zakończone sukcesem!", + "restore_failed": "Przywracanie nie powiodło się.", + "cache_title": "Zarządzanie Cache", + "cache_memory_entries": "Wpisy w pamięci", + "cache_file_entries": "Wpisy w plikach", + "cache_total_size": "Całkowity rozmiar", + "cache_clear_all": "Wyczyść cały cache", + "cache_clear_provider": "Wyczyść cache obecnego dostawcy", + "cache_cleanup_old": "Wyczyść stary cache (>24h)", + "cache_cleared": "Cache wyczyszczony", + "cache_provider_cleared": "Cache dostawcy wyczyszczony", + "cache_cleaned": "Oczyszczono", + "cache_prompt": "Wybierz działanie", + "cache_confirm_clear_all": "Wyczyścić cały cache?", + "shortcuts": "Skróty Klawiszowe", + "shortcuts_config": "Ustawienia Skrótów", + "shortcuts_hint": "Używaj pojedynczych klawiszy dla szybkiego dostępu v menu", + "shortcut_search": "Szukaj Anime", + "shortcut_downloads": "Pobrane", + "shortcut_watchlist": "Lista", + "shortcut_settings": "Ustawienia", + "shortcut_exit": "Wyjście", + "shortcut_next_episode": "Następny Odcinek", + "shortcut_prev_episode": "Poprzedni Odcinek", + "shortcut_back": "Wstecz", + "shortcut_help": "Pomoc", + "shortcuts_reset": "Resetuj do Domyślnych", + "shortcuts_reset_confirm": "Wszystkie skróty zostaną zresetowane. Czy jesteś pewien?", + "shortcuts_reset_success": "Skróty zresetowane.", + "enter_shortcut": "Wprowadź nowy klawisz skrótu", + "shortcut_single_char": "Skrót musi być pojedynczym znakiem.", + "shortcut_changed": "Skrót zmieniony." + }, + "setup": { + "welcome": "Witaj w Weeb CLI!", + "language_prompt": "Wybierz Język / Select Language", + "wizard_title": "Kreator Konfiguracji", + "checking_deps": "Sprawdzanie zależności...", + "checking_tool": "Sprawdzanie {tool}...", + "installing_tool": "Instalowanie {tool}...", + "check": "Sprawdzanie {tool}...", + "found": "Znaleziono: {path}", + "found_short": "Dostępne", + "not_found": "Nie znaleziono {tool}. Próba instalacji...", + "not_found_short": "Nie znaleziono", + "installing": "Instalowanie {tool}...", + "installed": "Zainstalowano", + "downloading": "Pobieranie {tool}...", + "success": "{tool} zainstalowany pomyślnie.", + "failed": "Błąd podczas instalacji {tool}.", + "failed_short": "Błąd", + "manual_required": "Automatyczna instalacja {tool} nie powiodła se. Zainstaluj ręcznie.", + "complete": "Konfiguracja zakończona!", + "location": "Narzędzia zainstalowane w: {path}", + "pkg_manager_try": "Próba użycia menedżera pakietów: {manager}..." + }, + "common": { + "error": "Błąd", + "processing": "Przetwarzanie...", + "continue_key": "Nacişnij Enter, aby kontynuować...", + "success": "Do widzenia!", + "enabled": "Włączone", + "disabled": "Wyłączone", + "network_error": "Brak połączenia z internetem!", + "ctrl_c_hint": "Wskazówka: Użyj Ctrl+C, aby wrócić. (Wyjście w Menu Głównym)", + "wip": "W trakcie prac...", + "cancelled": "Anulowano" + }, + "search": { + "prompt": "Wprowadź nazwę anime", + "searching": "Szukanie...", + "no_results": "Nie znaleziono wyników.", + "results": "Wyniki Wyszukiwania", + "cancel": "Anuluj", + "error": "Wyszukiwanie nie powiodło się.", + "recent": "Ostatnie Wyszukiwania" + }, + "details": { + "error_slug": "Błąd: Nieprawidłowe ID anime.", + "not_found": "Nie znaleziono szczegółów.", + "episode": "Odcinek", + "season": "Sezon", + "select_episode": "Wybierz Odcinek", + "select_season": "Wybierz Sezon", + "no_episodes": "Brak dostępnych odcinków.", + "selected": "Wybrano: {episode}", + "watch": "Oglądaj", + "download": "Pobierz", + "add_to_library": "Dodaj do Biblioteki", + "remove_from_library": "Usuń z Biblioteki", + "added_to_library": "Dodano do biblioteki!", + "already_in_library": "Już jest v bibliotece.", + "removed_from_library": "Usunięto z biblioteki.", + "mark_watched": "Oznaczyć jako obejrzane?", + "marked_watched": "Oznaczono jako obejrzane", + "watched_prefix": "✓ ", + "next_prefix": "● ", + "validating_streams": "Walidacja strumieni...", + "no_valid_streams": "Nie znaleziono prawidłowych strumieni.", + "streams_valid": "strumieni prawidłowych", + "sync_to_trackers": "Synchronizować również z trackerami?", + "action_prompt": "Wybierz Działanie", + "download_options": { + "all": "Wybierz Wszystkie", + "manual": "Wybór Ręczny", + "range": "Wybór Zakresu", + "prompt": "Wybierz tryb pobierania", + "range_input": "Wprowadź zakres (np. 1-5, 8)", + "started": "Pobieranie rozpoczęte...", + "range_error": "Nieprawidłowy format zakresu." + }, + "player_starting": "Uruchamianie Odtwarzacza...", + "stream_not_found": "Nie znaleziono URL strumienia.", + "select_source": "Wybierz Źródło:", + "invalid_ep_id": "Nieprawidłowe ID Odcinka.", + "download_wip": "Funkcja pobierania już vkrótce...", + "queueing": "Kolejkowanie {count} odcinków... (Vkrótce)" + }, + "player": { + "installing_mpv": "Nie znaleziono MPV. Instalowanie...", + "install_failed": "Instalacja MPV nie powiodła se. Zainstaluj ręcznie.", + "error": "Błąd odtwarzacza", + "resuming": "Wznawianie od {time}s", + "auto_marked": "Osiągnięto 80%, automatycznie oznaczono jako obejrzane." + }, + "watchlist": { + "total_anime": "Suma Anime", + "total_episodes": "Suma Odcinków", + "total_hours": "Całkowity Czas", + "last_watched": "Ostatnio Oglądane", + "completed": "Ukończone", + "in_progress": "W Trakcie", + "select_category": "Wybierz Kategorię", + "select_anime": "Wybierz Anime", + "no_completed": "Brak ukończonych anime.", + "no_in_progress": "Brak anime v trakcie oglądania.", + "anime_title": "Anime", + "episodes_watched": "Obejrzane", + "next": "Następny", + "tracker_updated": "zaktualizowano", + "tracker_syncing": "synchronizacja...", + "tracker_pending": "Dodano do oczekujących", + "tracker_error": "Błąd trackera" + }, + "errors": { + "generic": "Wystąpił błąd", + "provider": "Nie udało se pobrać danych ze źródła", + "download": "Pobieranie nie powiodło se", + "network": "Błąd połączenia sieciowego", + "provider_error": "Błąd dostawcy: {error}", + "timeout": "Przekroczono limit czasu połączenia", + "cloudflare": "Ochrona Cloudflare aktywna", + "no_streams": "Nie znaleziono strumieni" + }, + "downloads": { + "disk_full": "Niewystarczająca ilość miejsca na dysku: {free} dostępne. Wymagane co najmniej 1GB.", + "no_stream_url": "Nie znaleziono URL strumienia", + "stream_data_failed": "Nie udało se pobrać danych strumienia", + "empty_stream_links": "Brak dostępnych linków do strumienia", + "no_valid_streams_found": "Nie znaleziono prawidłowych strumieni po walidacji", + "title": "Pobrane", + "empty": "Nic jeszcze nie pobrałeś.", + "status": "Status", + "progress": "Postęp", + "status_processing": "Pobieranie", + "status_completed": "Ukończono", + "status_failed": "Błąd", + "status_pending": "Oczekujące", + "failed_downloads": "nieudane pobieranie(a)", + "retrying_downloads": "ponawianie pobierania...", + "default_folder_name": "Pobrane", + "downloads_folder": "Pobrane", + "resume_prompt": "Znaleziono {count} nieukończone pobieranie(a). Wznowić?", + "resumed": "Wznawianie pobierania...", + "already_downloading": "To anime jest już pobierane.", + "already_in_queue": "Wybrane odcinki są już v kolejce.", + "queued": "Dodano {count} odcinek(ów) do kolejki.", + "start_now": "Pobierz Teraz", + "add_to_queue": "Dodaj do Kolejki", + "action_prompt": "Wybierz Działanie", + "view_queue": "Zobacz Kolejkę", + "start_queue": "Uruchom Kolejkę", + "stop_queue": "Zatrzymaj Kolejkę", + "clear_completed": "Wyczyść Ukończone", + "queue_started": "Kolejka uruchomiona.", + "queue_stopped": "Kolejka zatrzymana.", + "queue_running": "Uruchomiona", + "pending_count": "{count} pobieranie(a) oczekujące", + "cleared": "Wyczyszczono.", + "completed_downloads": "Ukończone Pobierania", + "active_downloads": "Aktywne Pobierania", + "manage_queue": "Zarządzanie Kolejką", + "select_anime": "Wybierz Anime", + "search_library": "Szukaj", + "search_anime": "Wprowadź nazwę anime", + "search_all": "Szukaj ve wszystkich źródłach", + "offline": "Offline", + "reindex": "Indeksuj ponownie", + "indexing": "Indeksowanie...", + "indexed": "Zindeksowano {count} anime", + "no_indexed": "Brak zindeksowanych anime. Podłącz dysk i zindeksuj najpierw.", + "drive_not_connected": "Dysk niepodłączony", + "connect_drive": "Podłącz dysk {name}", + "notification_title": "Weeb CLI", + "notification_complete": "{anime} - Odcinek {episode} pobrany", + "retry_failed": "Ponów Nieudane", + "retrying": "Ponawianie {count} pobrań...", + "episode_short": "Odc", + "retrying_status": "Ponawianie ({attempt}/{max})...", + "ffmpeg_progress": "W {time}", + "stream_data_failed": "Nie udało se pobrać danych strumienia", + "empty_stream_links": "Linki strumienia są puste", + "no_valid_streams_found": "Nie znaleziono prawidłowych strumieni" + }, + "library": { + "title": "Moja Biblioteka", + "online_library": "Biblioteka Online", + "local_library": "Biblioteka Lokalna", + "select_type": "Wybierz Typ Biblioteki", + "online_empty": "Twoja biblioteka online jest pusta. Możesz dodać anime ze strony szczegółów.", + "local_empty": "Twoja biblioteka lokalna jest pusta. Pobrane anime pojawią se tutaj.", + "local_not_available": "To anime nie jest już dostępne. Dysk może być odłączony lub plik usunięty.", + "total_anime": "Suma Anime", + "select_anime": "Wybierz Anime", + "switching_provider": "Przełączanie na {provider}...", + "provider_not_found": "Dostawca {provider} nie został znaleziony." + } +} diff --git a/weeb_cli/locales/tr.json b/weeb_cli/locales/tr.json index 32eaef8..9868d7b 100644 --- a/weeb_cli/locales/tr.json +++ b/weeb_cli/locales/tr.json @@ -150,7 +150,7 @@ "restore_confirm": "Mevcut veritabanı yedeğin üzerine yazılacak. Emin misiniz?", "restore_success": "Geri yükleme başarılı!", "restore_failed": "Geri yükleme başarısız.", - "cache": "Önbellek Yönetimi", + "cache_title": "Önbellek Yönetimi", "cache_title": "Önbellek Yönetimi", "cache_memory_entries": "Bellekteki girişler", "cache_file_entries": "Dosyadaki girişler", diff --git a/weeb_cli/main.py b/weeb_cli/main.py index cc33f1d..4059c56 100644 --- a/weeb_cli/main.py +++ b/weeb_cli/main.py @@ -39,13 +39,14 @@ def run_setup(): langs = { "Türkçe": "tr", "English": "en", - "Deutsch": "de" + "Deutsch": "de", + "Polski": "pl" } choices = [(k, v) for k, v in langs.items()] selected_code = prompt.select( - "Select Language / Dil Seçiniz", + "", choices ) diff --git a/weeb_cli/providers/pl/__init__.py b/weeb_cli/providers/pl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/weeb_cli/providers/pl/docchi.py b/weeb_cli/providers/pl/docchi.py new file mode 100644 index 0000000..fb77c74 --- /dev/null +++ b/weeb_cli/providers/pl/docchi.py @@ -0,0 +1,110 @@ +from typing import Any, Dict, List, Optional + +from weeb_cli.providers.base import ( + AnimeDetails, + AnimeResult, + BaseProvider, + Episode, + StreamLink, +) +from weeb_cli.providers.registry import register_provider +from weeb_cli.services.logger import debug + + +@register_provider("docchi", lang="pl", region="PL") +class DocchiProvider(BaseProvider): + BASE_URL = "https://api.docchi.pl/v1" + + def search(self, query: str) -> List[AnimeResult]: + url = f"{self.BASE_URL}/series/list" + data = self._request(url) + + if not data or not isinstance(data, list): + return [] + + results = [] + for item in data: + title = item.get("title", "") + if query.lower() in title.lower(): + results.append( + AnimeResult( + id=item.get("slug", ""), + title=title, + cover=item.get("image", ""), + year=None, + ) + ) + + return results + + def get_details(self, anime_id: str) -> Optional[AnimeDetails]: + url = f"{self.BASE_URL}/series/find/{anime_id}" + data = self._request(url) + + if not data: + return None + + episodes = self.get_episodes(anime_id) + + return AnimeDetails( + id=anime_id, + title=data.get("title", ""), + description=data.get("description", ""), + cover=data.get("image", ""), + genres=data.get("genres", []), + status=data.get("status", ""), + episodes=episodes, + total_episodes=len(episodes), + ) + + def get_episodes(self, anime_id: str) -> List[Episode]: + url = f"{self.BASE_URL}/episodes/count/{anime_id}" + data = self._request(url) + + if not data or not isinstance(data, list): + return [] + + episodes = [] + for i, item in enumerate(data, 1): + episodes.append(Episode(id=str(i), number=i, title=f"Odcinek {i}")) + + return episodes + + def get_streams(self, anime_id: str, episode_id: str) -> List[StreamLink]: + url = f"{self.BASE_URL}/episodes/find/{anime_id}/{episode_id}" + debug(f"[DOCCHI] Requesting streams from: {url}") + data = self._request(url) + debug(f"[DOCCHI] API Response: {data}") + + if not data or not isinstance(data, list): + debug( + f"[DOCCHI] No valid data returned from API for {anime_id} ep {episode_id}" + ) + return [] + + streams = [] + for player in data: + hosting = player.get("player_hosting", "unknown") + translator = player.get("translator_title", "") + player_url = player.get("player", "") + + if "mega.nz" in player_url: + debug(f"[DOCCHI] Skipping MEGA link: {player_url}") + continue + + if "ebd.cda.pl" in player_url: + parts = player_url.split("/") + if len(parts) >= 5: + video_id = parts[-1] + player_url = f"https://www.cda.pl/video/{video_id}" + + server_name = f"{hosting} ({translator})" if translator else hosting + debug(f"[DOCCHI] Found player: {server_name} -> {player_url}") + + if player_url: + streams.append( + StreamLink(url=player_url, quality="auto", server=server_name) + ) + + debug(f"[DOCCHI] Returning {len(streams)} streams") + return streams diff --git a/weeb_cli/services/player.py b/weeb_cli/services/player.py index 8471882..ac6737f 100644 --- a/weeb_cli/services/player.py +++ b/weeb_cli/services/player.py @@ -187,10 +187,10 @@ def play(self, url: str, title: Optional[str] = None, start_time: Optional[int] cmd.append("--fs") cmd.append("--save-position-on-quit") - cmd.append("--really-quiet") - cmd.append("--no-terminal") + # cmd.append("--really-quiet") + # cmd.append("--no-terminal") - log_debug(f"[Player] MPV cmd: {' '.join(cmd[:5])}... ({len(cmd)} args)") + log_debug(f"[Player] MPV cmd: {' '.join(cmd)} ") try: if slug and platform.system() != "Windows": @@ -201,14 +201,15 @@ def play(self, url: str, title: Optional[str] = None, start_time: Optional[int] ) self._monitor_thread.start() - result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True) + result = subprocess.run(cmd, capture_output=True, text=True) if self._monitor_thread: self._stop_monitor.set() self._monitor_thread.join(timeout=1) - if result.returncode != 0 and result.stderr: - console.print(f"[red]{i18n.t('player.error')}: {result.stderr.strip()}[/red]") + if result.returncode != 0: + log_debug(f"[Player] MPV Error (Code {result.returncode}): {result.stderr.strip()}") + console.print(f"[red]{i18n.t('player.error')}[/red]") return result.returncode == 0 except FileNotFoundError as e: handle_error(e, "Player:MPV", f"{i18n.t('player.error')}: MPV not found at {self.mpv_path}") diff --git a/weeb_cli/services/stream_validator.py b/weeb_cli/services/stream_validator.py index 1f1f167..cf299df 100644 --- a/weeb_cli/services/stream_validator.py +++ b/weeb_cli/services/stream_validator.py @@ -1,45 +1,49 @@ +from typing import Any, Dict, List, Optional, Tuple + import requests -from typing import Optional, Tuple, List, Dict, Any + from weeb_cli.services.logger import debug + class StreamValidator: - @staticmethod - def validate_url(url: str, headers: Optional[Dict[str, str]] = None, timeout: int = 5) -> Tuple[bool, Optional[str]]: + def validate_url( + url: str, headers: Optional[Dict[str, str]] = None, timeout: int = 5 + ) -> Tuple[bool, Optional[str]]: if not url: return False, "Empty URL" - - if not url.startswith(('http://', 'https://')): + + if not url.startswith(("http://", "https://")): return False, "Invalid protocol" - + try: response = requests.head( - url, - headers=headers or {}, - timeout=timeout, - allow_redirects=True + url, headers=headers or {}, timeout=timeout, allow_redirects=True ) - + if response.status_code == 405: response = requests.get( - url, - headers=headers or {}, - timeout=timeout, - stream=True + url, headers=headers or {}, timeout=timeout, stream=True ) response.close() - + if response.status_code == 200: - content_type = response.headers.get('content-type', '').lower() - - valid_types = ['video/', 'application/x-mpegurl', 'application/vnd.apple.mpegurl', 'application/octet-stream'] + content_type = response.headers.get("content-type", "").lower() + + valid_types = [ + "video/", + "application/x-mpegurl", + "application/vnd.apple.mpegurl", + "application/octet-stream", + "text/html", + ] if any(t in content_type for t in valid_types) or not content_type: return True, None else: return False, f"Invalid content type: {content_type}" - + return False, f"HTTP {response.status_code}" - + except requests.Timeout: return False, "Connection timeout" except requests.ConnectionError: @@ -47,26 +51,35 @@ def validate_url(url: str, headers: Optional[Dict[str, str]] = None, timeout: in except Exception as e: debug(f"[VALIDATOR] Validation error: {e}") return False, str(e) - + @staticmethod - def validate_streams(streams: List[Any], headers: Optional[Dict[str, str]] = None) -> List[Any]: + def validate_streams( + streams: List[Any], headers: Optional[Dict[str, str]] = None + ) -> List[Any]: validated = [] - + for stream in streams: - url = stream.url if hasattr(stream, 'url') else stream.get('url') - stream_headers = stream.headers if hasattr(stream, 'headers') else stream.get('headers', {}) - + url = stream.url if hasattr(stream, "url") else stream.get("url") + stream_headers = ( + stream.headers + if hasattr(stream, "headers") + else stream.get("headers", {}) + ) + if headers: stream_headers.update(headers) - - is_valid, error = StreamValidator.validate_url(url, stream_headers, timeout=3) - + + is_valid, error = StreamValidator.validate_url( + url, stream_headers, timeout=3 + ) + if is_valid: validated.append(stream) debug(f"[VALIDATOR] Valid stream: {url[:50]}...") else: debug(f"[VALIDATOR] Invalid stream: {url[:50]}... - {error}") - + return validated + stream_validator: StreamValidator = StreamValidator()