diff --git a/README.md b/README.md index b61329f..18b8cd7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # aw-cli -

-Guarda anime dal terminale e molto altro!
Gli anime vengono presi da AnimeWorld +**Guarda anime dal terminale e molto altro.** -

+**Gli anime vengono presi da [AnimeWorld](https://www.animeworld.tv/)** ## Anteprima -https://github.com/fexh10/aw-cli/assets/90156014/88e1c2e2-bb7f-4002-8784-26f70861e164 + +[Guarda l'anteprima su GitHub](https://github.com/fexh10/aw-cli/assets/90156014/88e1c2e2-bb7f-4002-8784-26f70861e164) ## Indice @@ -14,119 +14,134 @@ https://github.com/fexh10/aw-cli/assets/90156014/88e1c2e2-bb7f-4002-8784-26f7086 - [Anteprima](#anteprima) - [Indice](#indice) - [Installazione](#installazione) + - [Linux e macOS](#linux-e-macos) + - [Windows](#windows) + - [Ultima versione (WSL)](#ultima-versione-wsl) + - [Versione Legacy](#versione-legacy) + - [Android](#android) + - [iOS](#ios) - [Problemi noti](#problemi-noti) + - [Linux / Windows WSL](#linux--windows-wsl) + - [macOS](#macos) + - [Windows Legacy](#windows-legacy) - [Disinstallazione](#disinstallazione) - [Utilizzo](#utilizzo) - [Crediti](#crediti) - ## Installazione -Lo script funziona sia con [MPV](https://mpv.io/installation/) che con [VLC](https://www.videolan.org/vlc/index.it.html).
+Lo script funziona sia con [MPV](https://mpv.io/installation/) che con [VLC](https://www.videolan.org/vlc/index.it.html). +È richiesta l'installazione di [fzf](https://github.com/junegunn/fzf?tab=readme-ov-file#installation). -È richiesta l'installazione di [fzf](https://github.com/junegunn/fzf?tab=readme-ov-file#installation).
+### Linux e macOS -
Linux, MacOS -È possibile installare aw-cli da pip: +È possibile installare **aw-cli** da `pip`: -``` +```sh python3 -m pip install aw-cli ``` -
- -
Windows -Attualmente, Windows presenta due versioni: la più recente, progettata per funzionare su WSL (Windows Subsystem for Linux), e una versione Legacy compatibile con PowerShell. La versione Legacy non riceverà ulteriori aggiornamenti, mentre l'altra sarà mantenuta costantemente. -
+### Windows -
Ultima Versione -L'ultima versione per Windows richiede installare WSL: +Attualmente Windows presenta due versioni: -``` -wsl --install -``` +- **Ultima versione** (funziona su [WSL](https://learn.microsoft.com/it-it/windows/wsl/install)) +- **Legacy** (compatibile con PowerShell, ma non più aggiornata) -Il programma dovrà essere installato e avviato da WSL: +#### Ultima versione (WSL) -``` +```sh +wsl --install python3 -m pip install aw-cli ``` -
-
Versione Legacy -Per installare la versione Legacy, è necessario avere git. +#### Versione Legacy +Richiede [Git per Windows](https://www.git-scm.com/download/win): -``` +```sh python3 -m pip install git+https://github.com/fexh10/aw-cli.git@winLegacy ``` -
-
+### Android -
Android -Android richiede l'installazione di termux.
+Richiede [Termux](https://github.com/termux/termux-app/releases/tag/v0.118.0). -``` +```sh pkg update && pkg upgrade pkg install python python-pip fzf python3 -m pip install aw-cli ``` -
-
iOS -La versione per iOS richiede iSH e VLC. +### iOS -``` +Richiede [iSH](https://apps.apple.com/it/app/ish-shell/id1436902243) e [VLC](https://apps.apple.com/it/app/vlc-media-player/id650377962). + +```sh apk update apk upgrade apk add python3 python3-dev py3-pip gcc musl-dev git python3 -m pip install git+https://github.com/fexh10/aw-cli.git@iosCompatibility ``` -Nota che la velocità di download e caricamento molto bassa è un problema di iSH e non di aw-cli. -
-## Problemi noti -Se è impossibile avviare `aw-cli`, è possibile che non si abbia la cartella degli script Python aggiunta al path.
+> In questo modo è necessario creare e attivare un ambiente virtuale prima di eseguire **aw-cli**. -
Linux/Windows WSL -Aggiungere la seguente linea al file di profilo (.bashrc, .zshrc, o altro): +Oppure, in alternativa: +```sh +apk update +apk upgrade +apk add python3 python3-dev py3-pip gcc musl-dev git bash +pip install pipx +pipx install git+https://github.com/fexh10/aw-cli.git@iosCompatibility ``` + +Prima di avviare **aw-cli**, è consigliato chiudere e riaprire l’app iSH. +La velocità di download e caricamento può essere ridotta a causa di iSH, non di **aw-cli**. + +## Problemi noti + +Se non riesci ad avviare `aw-cli`, probabilmente la cartella degli script Python non è nel tuo `PATH`. + +### Linux / Windows WSL + +Aggiungi al tuo `.bashrc` o `.zshrc`: + +```sh export PATH=$PATH:$HOME/.local/bin ``` -Riavviare il terminale o eseguire `source ~/.bashrc`. - -
-
MacOS -Aggiungere la seguente linea al file di profilo (.bashrc, .zshrc, o altro): +Poi riavvia il terminale o esegui: +```sh +source ~/.bashrc ``` + +### macOS + +```sh export PATH=$PATH:$HOME/Library/Python/3.x/bin ``` -Sostituire `3.x` con la propria versione di Python.
-Riavviare il terminale o eseguire `source ~/.bashrc`. -
-
Windows Legacy -Inserire da linea di comando: +Sostituisci `3.x` con la tua versione di Python, poi riavvia il terminale. +### Windows Legacy + +```sh +setx PATH "%PATH%;%APPDATA%\Local\Programs\Python\Python3x\Scripts" ``` -setx PATH "%PATH%;%APPDATA%\Local\Programs\Python\Python3x\Scripts -``` -Sostituire `3.x` con la propria versione di Python.
-Se necessario, riavviare il sistema. -
-## Disinstallazione +Sostituisci `3x` con la tua versione di Python e riavvia se necessario. -``` +## Disinstallazione + +```sh python3 -m pip uninstall aw-cli ``` ## Utilizzo -``` + +```sh usage: aw-cli [-h] [-v] [-c [{r}]] [-l [{a,s,d,t}]] [-i] [-s] [-d] [-o] [-p] [-u [UPDATE]] [-a] Guarda anime dal terminale e molto altro! @@ -136,23 +151,20 @@ Informazioni: -v, --versione stampa la versione del programma Opzioni: - -c [{r}], --cronologia [{r}] - continua a guardare un anime dalla cronologia. 'r' per rimuovere un anime (opzionale) - -l [{a,s,d,t}], --lista [{a,s,d,t}] - lista degli ultimi anime usciti su AnimeWorld. a = all, s = sub, d = dub, t = tendenze. Default 'a' + -c [{r}], --cronologia [{r}] continua a guardare un anime dalla cronologia. 'r' per rimuovere (opzionale) + -l [{a,s,d,t}], --lista [{a,s,d,t}] lista degli ultimi anime usciti. a=all, s=sub, d=dub, t=tendenze -i, --info visualizza le informazioni e la trama di un anime - -s, --syncplay usa syncplay per guardare un anime insieme ai tuoi amici - -d, --download scarica gli episodi che preferisci - -o, --offline apri gli episodi scaricati precedentemente direttamente dal terminale - -p, --privato guarda un episodio senza che si aggiorni la cronologia o AniList - -u [UPDATE], --update [UPDATE] - aggiorna il programma + -s, --syncplay usa syncplay per guardare un anime con amici + -d, --download scarica gli episodi preferiti + -o, --offline apri episodi scaricati dal terminale + -p, --privato guarda senza aggiornare la cronologia o AniList + -u [UPDATE], --update [UPDATE] aggiorna il programma Configurazione: - -a, --configurazione avvia il menu di configurazione + -a, --configurazione avvia il menu di configurazione ``` ## Crediti -Progetto ispirato a ani-cli. -Un ringraziamento speciale a axtrat per l'aiuto nella realizzazione del progetto. +Progetto ispirato a [ani-cli](https://github.com/pystardust/ani-cli). +Un ringraziamento speciale a [axtrat](https://github.com/axtrat) per l’aiuto nella realizzazione del progetto. diff --git a/awcli/run.py b/awcli/run.py index ab9ce5b..ff35ceb 100644 --- a/awcli/run.py +++ b/awcli/run.py @@ -1,6 +1,8 @@ import os import re import csv +import shutil +import subprocess from signal import signal, SIGINT from concurrent.futures import ThreadPoolExecutor from pySmartDL import SmartDL @@ -36,10 +38,16 @@ def fzf(elementi: list[str], prompt: str = "> ", multi: bool = False, cls: bool if cls: ut.my_print("", end="", cls=True) string = "\n".join(elementi) - comando = f"""fzf --tac --height={len(elementi) + 2} --cycle --ansi --tiebreak=begin --prompt="{prompt}" """ + comando_fzf = f"""fzf --tac --height={len(elementi) + 2} --cycle --ansi --tiebreak=begin --prompt="{prompt}" """ if multi: - comando += "--multi --bind 'ctrl-a:toggle-all'" - output = os.popen(f"""printf "{string}" | {comando}""").read().strip() + comando_fzf += "--multi --bind 'ctrl-a:toggle-all'" + + comando_completo = f"""printf "{string}" | {comando_fzf}""" + + # Esegue fzf, catturando solo lo stdout per ottenere la selezione, + # ma permettendo a fzf di usare stderr per disegnare l'interfaccia utente. + process = subprocess.run(comando_completo, shell=True, stdout=subprocess.PIPE, text=True, check=False) + output = process.stdout.strip() if esci and output == "": safeExit() @@ -172,11 +180,13 @@ def openSyncplay(url_ep: str, nome_video: str, progress: int) -> tuple[bool, int args = f'''--force-media-title="{nome_video}" --start="{progress}" --fullscreen --keep-open''' - if ut.configData["player"]["type"] == "vlc": + if ut.configData.get("player", {}).get("type") == "vlc": args = f'''--meta-title "{nome_video}" --start-time="{progress}" --fullscreen''' - try : - out = os.popen(f'''{ut.configData["syncplay"]["path"]} -d --language it "{url_ep}" -- {args} 2>&1''').read() + try: + command = f'''{ut.configData["syncplay"]["path"]} -d --language it "{url_ep}" -- {args}''' + result = subprocess.run(command, shell=True, capture_output=True, text=True, check=False) + out = result.stdout except UnicodeDecodeError: out = "" @@ -212,9 +222,11 @@ def openMPV(url_ep: str, nome_video: str, progress: int) -> tuple[bool, int]: os.system(f'''am start --user 0 -a android.intent.action.VIEW -d "{url_ep}" -n is.xyz.mpv/.MPVActivity > /dev/null 2>&1''') return True, 0 - out = os.popen(f'''{ut.configData["player"]["path"]} "{url_ep}" --force-media-title="{nome_video}" --start="{progress}" --fullscreen --keep-open 2>&1''') + command = f'''{ut.configData["player"]["path"]} "{url_ep}" --force-media-title="{nome_video}" --start="{progress}" --fullscreen --keep-open''' + result = subprocess.run(command, shell=True, capture_output=True, text=True, check=False) + out = result.stdout - res = re.findall(r'(\d+):(\d+):(\d+) / [\d:]+ \((\d+)%\)', out.read())[-1] + res = re.findall(r'(\d+):(\d+):(\d+) / [\d:]+ \((\d+)%\)', out)[-1] progress = (int(res[0]) * 3600) + (int(res[1]) * 60) + int(res[2]) return int(res[3]) >= completeLimit, progress @@ -439,7 +451,18 @@ def setupConfig() -> None: ut.my_print("", end="", cls=True) ut.my_print("AW-CLI - CONFIGURAZIONE", color="giallo") - player = "vlc" + if ut.nome_os not in ["Android", "iOS"]: + player = fzf(["vlc", "mpv"], "Scegli il player che vuoi utilizzare") + ut.configData["player"]["type"] = player + + player_type = ut.configData["player"]["type"] + path = shutil.which(player_type) + + if not path: + ut.my_print(f"Percorso di {player_type} non trovato, inseriscilo manualmente.", color="giallo") + ut.configData["player"]["path"] = ut.my_input(f"Path di {player_type}") + else: + ut.configData["player"]["path"] = path #anilist if fzf(["sì","no"], "Aggiornare automaticamente la watchlist con AniList? ") == "sì": @@ -510,7 +533,8 @@ def reloadCrono(cronologia: list[Anime]): testo.append(f"\033[0;3{colore}m{i + 1} \033[0;37m{a.name} [Ep {a.ep_corrente}/{a.ep_totali}]") if notSelected: - pid = os.popen("pgrep fzf").read().strip().split("\n") + process = subprocess.run(["pgrep", "fzf"], capture_output=True, text=True, check=False) + pid = process.stdout.strip().split("\n") os.system(f"kill {pid[-1]}") scelta_anime = fzf(testo, "Scegli un anime: ") @@ -610,6 +634,10 @@ def main(): if update: updateScript() + if not shutil.which("fzf"): + ut.my_print("Errore: fzf non è installato. Per favore installalo per continuare.", color="rosso") + exit(1) + try: with open(f"{os.path.dirname(__file__)}/aw-cronologia.csv", encoding='utf-8') as file: log = [riga for riga in csv.reader(file)] @@ -622,9 +650,24 @@ def main(): ut.getConfig() - openPlayer = lambda url_ep, nome_video, progress: os.system(f"printf \"\e]8;;vlc://%s\a~~~~~~~~~~~~~~~~~~~~\n~ Premi per aprire VLC ~\n~~~~~~~~~~~~~~~~~~~~\e]8;;\a\n\" \"{url_ep}\"") + if ut.nome_os == "iOS": + def openPlayer_iOS(url_ep, nome_video, progress): + os.system(f"printf \"\e]8;;vlc://%s\a~~~~~~~~~~~~~~~~~~~~\n~ Premi per aprire VLC ~\n~~~~~~~~~~~~~~~~~~~~\e]8;;\a\n\" \"{url_ep}\"") + ut.my_input("Premi invio per continuare...", format=lambda _: True) + openPlayer = openPlayer_iOS + else: + player_type = ut.configData.get("player", {}).get("type") + if player_type == "vlc": + openPlayer = openVLC + elif player_type == "mpv": + openPlayer = openMPV + elif ut.nome_os == "Android": + openPlayer = openVLC + else: + ut.my_print("Player non configurato. Eseguire `aw-cli --config`.", color="rosso") + safeExit() - if nome_os != "Android" and args.syncpl: + if ut.nome_os not in ["Android", "iOS"] and getattr(args, "syncpl", False): openPlayer = openSyncplay reload = True diff --git a/awcli/utilities.py b/awcli/utilities.py index d9c1b7f..bd0723e 100644 --- a/awcli/utilities.py +++ b/awcli/utilities.py @@ -2,23 +2,36 @@ import re import toml import requests +import warnings +import subprocess from time import sleep from html import unescape from collections import defaultdict from awcli.anime import Anime +from urllib3.exceptions import InsecureRequestWarning -_url = "https://www.animeworld.so" +_url = "https://www.animeworld.ac" configData = defaultdict(dict) +_ssl_warning_shown = False + +# Inizializza la sessione di requests +session = requests.Session() +session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36' +}) # controllo il tipo del dispositivo def get_os() -> str: - out = os.popen("uname -a").read().strip().split() + result = subprocess.run(["uname", "-a"], capture_output=True, text=True, check=False) + out = result.stdout.strip().split() nome_os = out[0] if nome_os == "Linux": if "Android" == out[-1]: nome_os = "Android" elif "WSL" in out[2]: nome_os = "WSL" + elif len(out) > 1 and any(x in out[1].lower() for x in ["iphone", "ipad", "ipod"]): + nome_os = "iOS" return nome_os nome_os = get_os() @@ -78,23 +91,33 @@ def getHtml(url: str) -> str: Returns: str: l'html della pagina web selezionata. """ - global cookies + global _ssl_warning_shown try: - result = requests.get(url, headers=headers, cookies=cookies) - except requests.exceptions.ConnectionError: - my_print("Errore di connessione", color="rosso") + # Sopprime l'avviso di richiesta non sicura per evitare output duplicati + warnings.filterwarnings('ignore', category=InsecureRequestWarning) + + if not _ssl_warning_shown: + my_print("ATTENZIONE: La verifica del certificato SSL è disabilitata. Questo potrebbe esporre a rischi di sicurezza.", color="giallo") + _ssl_warning_shown = True + + result = session.get(url, timeout=10, verify=False) + result.raise_for_status() # Lancia un'eccezione per status code non 2xx + + # Gestione del reindirizzamento di Cloudflare + if result.status_code == 202 and "SecurityAW" in result.text: + my_print("Reindirizzamento...", color="giallo", end="\n") + result = session.get(url, timeout=10, verify=False) + result.raise_for_status() + + except requests.exceptions.RequestException as e: + my_print(f"Errore di connessione: {e}", color="rosso") exit() + finally: + # Ripristina il comportamento predefinito degli avvisi + warnings.resetwarnings() - if result.status_code == 202: - my_print("Reindirizzamento...", color="giallo", end="\n") - match = re.search(r'(SecurityAW-\w+)=(.*) ;', result.text) - key = match.group(1) - value = match.group(2) - cookies = {key: value} - result = requests.get(url, headers=headers, cookies=cookies) - if result.status_code != 200: - my_print("Errore: pagina non trovata", color="rosso") + my_print(f"Errore: pagina non trovata (status code: {result.status_code})", color="rosso") exit() return result.text @@ -121,7 +144,7 @@ def search(input: str) -> list[Anime]: # prendo i link degli anime relativi alla ricerca for url, name in re.findall(r'
(?:.|\n)+?([^<]+)', html): if nome_os == "Android": - caratteri_proibiti = '"*/:<>?\|' + caratteri_proibiti = r'"*/:<>?\|' caratteri_rimpiazzo = '”⁎∕꞉‹›︖\⏐' for a, b in zip(caratteri_proibiti, caratteri_rimpiazzo): name = name.replace(a, b) @@ -262,10 +285,4 @@ def getConfig() -> None: if nome_os == "WSL": configData["player"]["path"] = f'''"$(wslpath '{configData["player"]["path"]}')"''' if "syncplay" in configData: - configData["syncplay"]["path"] = f"/mnt/c/Windows/System32/cmd.exe /C '{configData["syncplay"]["path"]}'" - -headers = { - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36' -} - -cookies = {} \ No newline at end of file + configData["syncplay"]["path"] = f'''/mnt/c/Windows/System32/cmd.exe /C '{configData["syncplay"]["path"]}' ''' \ No newline at end of file diff --git a/tests/test_anime.py b/tests/test_anime.py index a8b4308..f91a848 100644 --- a/tests/test_anime.py +++ b/tests/test_anime.py @@ -12,6 +12,11 @@ def mock_download(): mock_download.return_value = "episode_content" yield mock_download +@pytest.fixture +def mock_search(): + with patch("awcli.utilities.search") as mock_search: + yield mock_search + @pytest.fixture def mock_get_info_anime(): with patch("awcli.utilities.get_info_anime") as mock_get_info_anime: @@ -51,11 +56,13 @@ def test_load_info(anime, mock_get_info_anime): assert anime.views == 232023 assert anime.plot == "trama" -def test_load_info_error(anime, mock_get_info_anime): +def test_load_info_error(anime, mock_get_info_anime, mock_search): mock_get_info_anime.side_effect = IndexError + mock_search.return_value = [] with pytest.raises(IndexError): anime.load_info() - assert mock_get_info_anime.call_count == 2 + assert mock_get_info_anime.call_count == 1 + assert mock_search.call_count == 1 def test_get_episodio(anime, mock_get_info_anime, mock_download): diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 5e2b2b0..96c86a9 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -9,7 +9,7 @@ # Definisce la fixture mock_get @pytest.fixture def mock_get(): - with patch('requests.get') as mock: + with patch('awcli.utilities.session.get') as mock: mock.return_value.status_code = 200 yield mock @@ -19,28 +19,28 @@ def mock_get(): ("empty", []), ]) def test_search(mock_get, input, expected): - input = TEST_DIR + "/" + input - with open(input, 'r') as html: + input_file = TEST_DIR + "/" + input + with open(input_file, 'r') as html: mock_get.return_value.text = html.read() - results = [anime.name for anime in utilities.search(input)] + results = [anime.name for anime in utilities.search("dummy_search")] assert results == expected def test_download(mock_get): - input = TEST_DIR + "/" + "theeminenceinshadowep13" - with open(input) as html: + input_file = TEST_DIR + "/" + "theeminenceinshadowep13" + with open(input_file) as html: mock_get.return_value.text = html.read() - results = utilities.download(input) - expected = "https://server18.streamingaw.online/DDL/ANIME/KageNoJitsuryokushaNiNaritakute/KageNoJitsuryokushaNiNaritakute_Ep_13_SUB_ITA.mp4" - ##assert results == expected + results = utilities.download("https://www.animeworld.ac/play/the-eminence-in-shadow/Dcqo-Q") + expected = "https://srv13-hana.sweetpixel.org/DDL/ANIME/KageNoJitsuryokushaNiNaritakute/KageNoJitsuryokushaNiNaritakute_Ep_13_SUB_ITA.mp4" + assert results == expected def test_get_info_anime(mock_get): - input = TEST_DIR + "/" + "theeminenceinshadowep13" - with open(input) as html: + input_file = TEST_DIR + "/" + "theeminenceinshadowep13" + with open(input_file) as html: mock_get.return_value.text = html.read() - results = utilities.get_info_anime(input) + results = utilities.get_info_anime("https://www.animeworld.ac/play/the-eminence-in-shadow/pzm5jA") expected = [ "Anime", "Giapponese", @@ -71,5 +71,4 @@ def test_my_input(input_mock, input_str, format_func, input_values, expected_out # Esegue il test result = utilities.my_input(input_str, format_func) # Verifica che il risultato sia corretto - assert result == expected_output - + assert result == expected_output \ No newline at end of file