aw-cli 2.3__tar.gz → 2.3.2__tar.gz

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.
Files changed (47) hide show
  1. aw_cli-2.3.2/.github/ISSUE_TEMPLATE/bug_report.md +65 -0
  2. {aw_cli-2.3 → aw_cli-2.3.2}/.gitignore +3 -0
  3. {aw_cli-2.3 → aw_cli-2.3.2}/PKG-INFO +3 -2
  4. {aw_cli-2.3 → aw_cli-2.3.2}/pyproject.toml +8 -1
  5. {aw_cli-2.3/src/aw_cli/fzf → aw_cli-2.3.2/src/aw_cli/interface}/fzf.py +31 -10
  6. aw_cli-2.3.2/src/aw_cli/providers/__init__.py +36 -0
  7. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/providers/animeunity.py +14 -14
  8. aw_cli-2.3.2/src/aw_cli/providers/animeworld.py +217 -0
  9. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/providers/local.py +1 -1
  10. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/providers/provider.py +24 -12
  11. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/run.py +274 -119
  12. aw_cli-2.3.2/tests/conftest.py +128 -0
  13. aw_cli-2.3.2/tests/core/__init__.py +0 -0
  14. aw_cli-2.3.2/tests/interface/__init__.py +0 -0
  15. {aw_cli-2.3/tests → aw_cli-2.3.2/tests/interface}/test_fzf.py +36 -5
  16. aw_cli-2.3.2/tests/providers/__init__.py +0 -0
  17. aw_cli-2.3.2/tests/providers/test_animeunity.py +90 -0
  18. aw_cli-2.3.2/tests/providers/test_animeworld.py +90 -0
  19. aw_cli-2.3.2/tests/providers/test_provider.py +42 -0
  20. aw_cli-2.3.2/uv.lock +759 -0
  21. aw_cli-2.3/src/aw_cli/providers/__init__.py +0 -12
  22. aw_cli-2.3/src/aw_cli/providers/animeworld.py +0 -122
  23. aw_cli-2.3/tests/empty +0 -25
  24. aw_cli-2.3/tests/search_dragonball +0 -216
  25. aw_cli-2.3/tests/search_onepiece +0 -127
  26. aw_cli-2.3/tests/theeminenceinshadowep13 +0 -499
  27. aw_cli-2.3/uv.lock +0 -749
  28. {aw_cli-2.3 → aw_cli-2.3.2}/.github/workflows/python-publish.yml +0 -0
  29. {aw_cli-2.3 → aw_cli-2.3.2}/.github/workflows/release.yml +0 -0
  30. {aw_cli-2.3 → aw_cli-2.3.2}/.pre-commit-config.yaml +0 -0
  31. {aw_cli-2.3 → aw_cli-2.3.2}/LICENSE +0 -0
  32. {aw_cli-2.3 → aw_cli-2.3.2}/README.md +0 -0
  33. {aw_cli-2.3 → aw_cli-2.3.2}/disclaimer.md +0 -0
  34. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/__init__.py +0 -0
  35. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/anilist.py +0 -0
  36. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/anime.py +0 -0
  37. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/arg_parser.py +0 -0
  38. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/download.py +0 -0
  39. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/history.py +0 -0
  40. {aw_cli-2.3/src/aw_cli/fzf → aw_cli-2.3.2/src/aw_cli/interface}/__init__.py +0 -0
  41. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/update.py +0 -0
  42. {aw_cli-2.3 → aw_cli-2.3.2}/src/aw_cli/utilities.py +0 -0
  43. {aw_cli-2.3 → aw_cli-2.3.2}/tests/__init__.py +0 -0
  44. {aw_cli-2.3/tests → aw_cli-2.3.2/tests/core}/test_anime.py +0 -0
  45. {aw_cli-2.3/tests → aw_cli-2.3.2/tests/core}/test_history.py +0 -0
  46. {aw_cli-2.3/tests → aw_cli-2.3.2/tests/core}/test_run.py +0 -0
  47. {aw_cli-2.3/tests → aw_cli-2.3.2/tests/interface}/test_rich_integration.py +0 -0
@@ -0,0 +1,65 @@
1
+ ## Descrizione del problema
2
+
3
+ Descrivi chiaramente il problema riscontrato.
4
+
5
+ ## Passi per riprodurre
6
+
7
+ Esempio:
8
+
9
+ 1. Esegui `aw-cli ...`
10
+ 2. Seleziona ...
11
+ 3. Il problema si verifica quando ...
12
+
13
+ ## Informazioni sull'ambiente
14
+
15
+ ### Versione di aw-cli
16
+
17
+ Esegui:
18
+
19
+ ```bash
20
+ aw-cli -v
21
+ ```
22
+
23
+ Output:
24
+
25
+ ```text
26
+ ...
27
+ ```
28
+
29
+ ### Sistema operativo
30
+
31
+ Esempio: Fedora 40, Ubuntu 24.04, Arch Linux, macOS, Android, Windows WSL, ecc.
32
+
33
+ ```text
34
+ ...
35
+ ```
36
+
37
+ ### Metodo di installazione
38
+
39
+ - [ ] `uv`
40
+ - [ ] `pipx`
41
+ - [ ] `pip`
42
+ - [ ] Installazione manuale / da sorgente
43
+ - [ ] Altro (specifica qui)
44
+
45
+ ### Provider
46
+
47
+ - [ ] AnimeWorld
48
+ - [ ] AnimeUnity
49
+
50
+ ### Player
51
+
52
+ - [ ] mpv
53
+ - [ ] vlc
54
+
55
+ ## Log / output del terminale
56
+
57
+ Incolla qui l'output completo dell'errore, se presente.
58
+
59
+ ```
60
+ ...
61
+ ```
62
+
63
+ ## Informazioni aggiuntive
64
+
65
+ Aggiungi qualsiasi altro dettaglio utile, screenshot o contesto.
@@ -88,3 +88,6 @@ pyrightconfig.json
88
88
  history.json
89
89
  config.toml
90
90
  aw-cronologia.csv
91
+
92
+ # Fixtures for tests
93
+ tests/fixtures/
@@ -1,7 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aw-cli
3
- Version: 2.3
3
+ Version: 2.3.2
4
4
  Summary: Guarda anime dal terminale e molto altro!
5
+ Project-URL: Homepage, https://github.com/fexh10/aw-cli
6
+ Project-URL: Repository, https://github.com/fexh10/aw-cli
5
7
  License-Expression: GPL-3.0
6
8
  License-File: LICENSE
7
9
  Requires-Python: >=3.10
@@ -11,7 +13,6 @@ Requires-Dist: pydantic>=2.12.5
11
13
  Requires-Dist: regex>=2025.9.18
12
14
  Requires-Dist: rich>=13.9.4
13
15
  Requires-Dist: toml>=0.10.2
14
- Requires-Dist: wheel>=0.45.1
15
16
  Description-Content-Type: text/markdown
16
17
 
17
18
  # aw-cli
@@ -12,9 +12,12 @@ dependencies = [
12
12
  "regex>=2025.9.18",
13
13
  "rich>=13.9.4",
14
14
  "toml>=0.10.2",
15
- "wheel>=0.45.1",
16
15
  ]
17
16
 
17
+ [project.urls]
18
+ Homepage = "https://github.com/fexh10/aw-cli"
19
+ Repository = "https://github.com/fexh10/aw-cli"
20
+
18
21
  [project.scripts]
19
22
  aw-cli = "aw_cli.run:main"
20
23
 
@@ -31,3 +34,7 @@ dev = [
31
34
  "pyright>=1.1.406",
32
35
  "pytest>=8.4.2",
33
36
  ]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
40
+ addopts = "-ra"
@@ -1,11 +1,11 @@
1
1
  import subprocess
2
2
  import sys
3
3
  import re
4
+ import socket
5
+ import json
4
6
  import argparse
5
7
  import urllib.request
6
8
 
7
- import aw_cli.utilities as ut
8
-
9
9
 
10
10
  class Fzf:
11
11
  """
@@ -20,7 +20,7 @@ class Fzf:
20
20
  }
21
21
 
22
22
  def __init__(self):
23
- self._port: int = ut.config_data.get("fzf", {}).get("port", 4321)
23
+ self._port: int = 0
24
24
 
25
25
  def run(
26
26
  self,
@@ -38,7 +38,10 @@ class Fzf:
38
38
  multi: se True abilita la selezione multipla (Ctrl+A = toggle all).
39
39
  filter: se True abilita il filtro per range (formato: inizio-fine).
40
40
  """
41
- cmd = self._build_cmd(elements=elements, prompt=prompt, multi=multi, filter=filter)
41
+ self._port = self._find_free_port()
42
+ cmd = self._build_cmd(
43
+ elements=elements, prompt=prompt, multi=multi, filter=filter
44
+ )
42
45
 
43
46
  try:
44
47
  while True:
@@ -82,6 +85,16 @@ class Fzf:
82
85
  except OSError:
83
86
  pass
84
87
 
88
+ @staticmethod
89
+ def _find_free_port() -> int:
90
+ """
91
+ Restituisce una porta TCP libera assegnata dal sistema operativo.
92
+ Usare questa porta per --listen evita conflitti e TIME_WAIT tra riavvii di fzf.
93
+ """
94
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
95
+ s.bind(("", 0))
96
+ return s.getsockname()[1]
97
+
85
98
  def _build_cmd(
86
99
  self,
87
100
  elements: list[str],
@@ -109,11 +122,12 @@ class Fzf:
109
122
  cmd += ["--multi", "--bind", "ctrl-a:toggle-all"]
110
123
 
111
124
  if filter:
112
- reload_cmd = f"{sys.executable} -m aw_cli.fzf.fzf --filter {{q}} --episodes \"{elements}\""
125
+ reload_cmd = f"{sys.executable} -m aw_cli.interface.fzf --filter {{q}} --episodes '{json.dumps(elements)}'"
113
126
  cmd += [
114
127
  "--phony",
115
- "--bind", f"change:reload({reload_cmd})",
116
- "--header=Range: inizio-fine. crtl+A seleziona tutto, tab/shift+tab selezione singola"
128
+ "--bind",
129
+ f"change:reload({reload_cmd})",
130
+ "--header=Range: inizio-fine. crtl+A seleziona tutto, tab/shift+tab selezione singola",
117
131
  ]
118
132
 
119
133
  return cmd
@@ -132,7 +146,10 @@ def _filter_episodes(query: str, episodes_raw: str) -> None:
132
146
  ha successo se i due intervalli si intersecano.
133
147
  """
134
148
 
135
- episodes: list[str] = eval(episodes_raw)
149
+ try:
150
+ episodes: list[str] = json.loads(episodes_raw)
151
+ except (json.JSONDecodeError, ValueError):
152
+ return
136
153
 
137
154
  if not query:
138
155
  print("\n".join(episodes))
@@ -185,8 +202,12 @@ def _filter_episodes(query: str, episodes_raw: str) -> None:
185
202
 
186
203
  if __name__ == "__main__":
187
204
  parser = argparse.ArgumentParser()
188
- parser.add_argument("--filter", type=str, default="", help="Query per filtrare gli episodi")
189
- parser.add_argument("--episodes", type=str, required=True, help="Lista episodi JSON")
205
+ parser.add_argument(
206
+ "--filter", type=str, default="", help="Query per filtrare gli episodi"
207
+ )
208
+ parser.add_argument(
209
+ "--episodes", type=str, required=True, help="Lista episodi JSON"
210
+ )
190
211
  args = parser.parse_args()
191
212
 
192
213
  _filter_episodes(args.filter, args.episodes)
@@ -0,0 +1,36 @@
1
+ # pyright: reportUnusedImport=false
2
+ from .provider import Provider
3
+
4
+ PROVIDERS_AVAILABLE = {
5
+ "animeunity": ("aw_cli.providers.animeunity", "Animeunity"),
6
+ "animeworld": ("aw_cli.providers.animeworld", "Animeworld"),
7
+ # "animeworld-api": ("aw_cli.providers.animeworld_api", "AnimeWorldAPI"),
8
+ }
9
+
10
+ def create_provider(name: str) -> Provider:
11
+ """
12
+ Crea un'istanza del provider in base al nome.
13
+ Usa importazione lazy per caricare solo il modulo necessario.
14
+
15
+ Args:
16
+ name (str): il nome del provider (es. 'animeunity', 'animeworld').
17
+
18
+ Returns:
19
+ Provider: l'istanza del provider creato.
20
+
21
+ Raises:
22
+ ValueError: se il nome del provider non è disponibile.
23
+ """
24
+ if name not in PROVIDERS_AVAILABLE:
25
+ raise ValueError(f"Provider '{name}' non disponibile. Scegli tra: {list(PROVIDERS_AVAILABLE.keys())}")
26
+
27
+ module_name, class_name = PROVIDERS_AVAILABLE[name]
28
+ import importlib
29
+ module = importlib.import_module(module_name)
30
+ provider_class = getattr(module, class_name)
31
+ return provider_class()
32
+
33
+ __all__ = [
34
+ "Provider",
35
+ "create_provider",
36
+ ]
@@ -1,38 +1,38 @@
1
1
  import re
2
2
  import json
3
3
  from html import unescape
4
+ from functools import cached_property
4
5
  from ..anime import Anime, AnimeStatus
5
6
  from .provider import Provider, HTTPError
6
7
 
7
8
  class Animeunity(Provider):
8
9
  """
9
10
  Classe che gestisce il collegamento con Animeworld.
10
-
11
+
11
12
  Attributes:
12
13
  _url (str): l'url del sito di Animeworld.
13
14
  _headers (dict): gli headers da utilizzare per le richieste HTTP.
14
15
  _cookies (dict): i cookies da utilizzare per le richieste HTTP.
15
16
  _visited (dict): le pagine già visitate.
16
17
  """
17
- def __init__(self):
18
- super().__init__("https://www.animeunity.so")
19
- self._get_token() # Assicura che i token/cookie siano aggiornati
18
+ def __init__(self, client=None):
19
+ super().__init__("https://www.animeunity.so", client=client)
20
20
 
21
-
22
- def _get_token(self) -> None:
23
- # Send a GET request to the specified URL
21
+ @cached_property
22
+ def _session(self) -> str:
23
+ """Inizializza la sessione (token CSRF + cookies) al primo accesso. Ritorna l'HTML."""
24
24
  response = self.Client.get(self.BASE_URL)
25
- self.html = response.text
26
- # Regex to find the meta tag with csrf-token and extract the content
27
- csrf_match = re.search(r'<meta.*?name="csrf-token".*?content="([^"]*)".*?>', self.html)
25
+ html = response.text
26
+ csrf_match = re.search(r'<meta.*?name="csrf-token".*?content="([^"]*)".*?>', html)
28
27
  if not csrf_match:
29
28
  raise ValueError("CSRF token not found in the HTML")
30
- csrf_token = csrf_match.group(1)
31
- self.Client.headers['x-csrf-token'] = csrf_token
29
+ self.Client.headers['x-csrf-token'] = csrf_match.group(1)
32
30
  self.Client.cookies['animeunity_session'] = response.cookies.get('animeunity_session') or ""
31
+ return html
33
32
 
34
33
 
35
34
  def _search(self, input: str) -> list[Anime]:
35
+ _ = self._session # trigger lazy init
36
36
  search_url = f"{self.BASE_URL}/livesearch"
37
37
  response = self.Client.post(
38
38
  url=search_url,
@@ -52,7 +52,7 @@ class Animeunity(Provider):
52
52
 
53
53
 
54
54
  def _latest(self, filter, specials) -> list[Anime]:
55
- regex = re.search(r'<layout-items[^>]*items-json="([^"]*)"', self.html)
55
+ regex = re.search(r'<layout-items[^>]*items-json="([^"]*)"', self._session)
56
56
  if not regex:
57
57
  raise ValueError("Errore nel parsing della pagina principale")
58
58
 
@@ -131,7 +131,7 @@ class Animeunity(Provider):
131
131
 
132
132
  def _parse_info(self, data: dict) -> tuple[str, str, int, AnimeStatus, dict[str, str]]:
133
133
  title = data['title_eng'] or data['title'] or data['title_it']
134
- last_ep = str(data['real_episodes_count']) if 'real_episodes_count' in data else "0"
134
+ last_ep = str(data['real_episodes_count']) if 'real_episodes_count' in data else "0"
135
135
  anilist_id = data['anilist_id']
136
136
  match data['status']:
137
137
  case "In Corso":
@@ -0,0 +1,217 @@
1
+ from functools import lru_cache
2
+ import re
3
+ from html import unescape
4
+ from urllib.parse import quote_plus
5
+ from ..anime import Anime, AnimeStatus
6
+ from .provider import Provider, HTTPError
7
+
8
+
9
+ class Animeworld(Provider):
10
+ """
11
+ Classe che gestisce il collegamento con Animeworld.
12
+
13
+ Attributes:
14
+ _url (str): l'url del sito di Animeworld.
15
+ _headers (dict): gli headers da utilizzare per le richieste HTTP.
16
+ _cookies (dict): i cookies da utilizzare per le richieste HTTP.
17
+ _visited (dict): le pagine già visitate.
18
+ """
19
+
20
+ def __init__(self, client=None):
21
+ super().__init__("https://www.animeworld.ac", client=client)
22
+ self._visited = {}
23
+
24
+ @lru_cache
25
+ def _get_html(self, url: str) -> str:
26
+ """
27
+ Ottiene l'html della pagina web `url`.
28
+ Gestisce cookie JavaScript (document.cookie) e imposta il csrf-token se presente.
29
+ """
30
+ if not re.match(r"^https?://", url):
31
+ raise HTTPError("Errore 404: pagina non trovata")
32
+
33
+ response = self.Client.get(url)
34
+
35
+ # Gestione cookie impostati tramite JavaScript (document.cookie)
36
+ cookie_match = re.search(
37
+ r'document\.cookie\s*?=\s*?"([^=]+)=([^";]+)', response.text
38
+ )
39
+ if cookie_match:
40
+ name, value = cookie_match.groups()
41
+ self.Client.cookies.set(name, value)
42
+ response = self.Client.get(url)
43
+
44
+ response.raise_for_status()
45
+
46
+ # Estrai e aggiorna il csrf-token se presente nella pagina per le chiamate API
47
+ csrf_token_match = re.search(
48
+ r'<meta.*?id="csrf-token"\s*?content="(.*?)">', response.text
49
+ )
50
+ if csrf_token_match:
51
+ self.Client.headers["csrf-token"] = csrf_token_match.group(1)
52
+
53
+ if response.status_code != 200 or "Errore 404" in response.text:
54
+ raise HTTPError("Errore 404: pagina non trovata")
55
+
56
+ return response.text
57
+
58
+ def _search(self, input: str) -> list[Anime]:
59
+ # Assicuriamoci che il client abbia cookies/csrf-token caricando prima la homepage
60
+ if "csrf-token" not in self.Client.headers:
61
+ try:
62
+ self._get_html(self.BASE_URL)
63
+ except Exception:
64
+ pass
65
+
66
+ response = self.Client.post(
67
+ f"{self.BASE_URL}/api/search/v2", params={"keyword": input}
68
+ )
69
+ response.raise_for_status()
70
+
71
+ data = response.json()
72
+ if "error" in data or "animes" not in data:
73
+ return []
74
+
75
+ animes = []
76
+ for elem in data["animes"]:
77
+ link = elem.get("link")
78
+ identifier = elem.get("identifier")
79
+ if link and identifier:
80
+ url = f"{self.BASE_URL}/play/{link}.{identifier}"
81
+ else:
82
+ url = self.BASE_URL
83
+
84
+ name = elem.get("name", "")
85
+ anime = Anime(unescape(name), url)
86
+
87
+ # Pre-populate all metadata from search JSON using a clean helper
88
+ anilist_id, status, info = self._parse_info(elem)
89
+ anime.set_info(anilist_id, status, info)
90
+ animes.append(anime)
91
+ return animes
92
+
93
+ def _latest(self, filter: str, specials: bool) -> list[Anime]:
94
+ html = self._get_html(self.BASE_URL)
95
+ animes = list[Anime]()
96
+
97
+ # Trova tutti i tag <a> con classe "poster" per estrarre in modo robusto
98
+ for a_tag in re.findall(r'<a\s+[^>]*class="poster"[^>]*>', html):
99
+ href_match = re.search(r'href="([^"]+)"', a_tag)
100
+ title_match = re.search(r'title="([^"]+)"', a_tag)
101
+ if not href_match or not title_match:
102
+ continue
103
+
104
+ url = href_match.group(1)
105
+ title = title_match.group(1)
106
+
107
+ # Estrae l'episodio dal titolo (es: "Nome Anime Ep 9")
108
+ ep = "0"
109
+ ep_match = re.search(r' Ep[\.\s]+([^"]+)$', title)
110
+ if ep_match:
111
+ ep = ep_match.group(1).strip()
112
+ name = title[: ep_match.start()].strip()
113
+ else:
114
+ name = title
115
+
116
+ parts = url.strip("/").split("/")
117
+ if len(parts) >= 3:
118
+ anime_slug = parts[1]
119
+ player_id = parts[2]
120
+ anime_ref = f"{self.BASE_URL}/play/{anime_slug}"
121
+ ep_ref = player_id
122
+ else:
123
+ anime_ref = self.BASE_URL + url
124
+ ep_ref = self.BASE_URL + url
125
+
126
+ anime = Anime(unescape(name), anime_ref, ep)
127
+ anime.update_episodes({ep: ep_ref}, specials=specials)
128
+ animes.append(anime)
129
+
130
+ BLOCK_SIZE = 45
131
+ filter_keys = ["a", "s", "d", "t"]
132
+ multiplier = filter_keys.index(filter[0])
133
+
134
+ start = multiplier * BLOCK_SIZE
135
+ end = (multiplier + 1) * BLOCK_SIZE if multiplier < len(filter_keys) else None
136
+
137
+ return animes[start:end]
138
+
139
+ def _episodes(self, anime: Anime):
140
+ html = self._get_html(anime.ref)
141
+ episodes_url = dict[str, str]()
142
+
143
+ # Estrae i tag <a> in modo super stabile limitandosi agli elementi con classe "episode"
144
+ for a_tag in re.findall(r'<li class="episode">[\s\n]*(<a\s+[^>]+>)', html):
145
+ id_match = re.search(r'data-id="([^"]+)"', a_tag)
146
+ num_match = re.search(r'data-num="([^"]+)"', a_tag)
147
+ if id_match and num_match:
148
+ episodes_url[num_match.group(1)] = id_match.group(1)
149
+ return episodes_url
150
+
151
+ def _episode_link(self, anime: Anime, episode: Anime.Episode) -> str:
152
+ player_id = episode.ref
153
+ if "play/" in player_id:
154
+ player_id = player_id.split("/")[-1]
155
+
156
+ api_url = f"{self.BASE_URL}/api/episode/serverPlayerAnimeWorld?id={player_id}"
157
+ response = self.Client.get(api_url)
158
+ response.raise_for_status()
159
+
160
+ source_match = re.search(r'<source src="([^"]+)"', response.text)
161
+ if not source_match:
162
+ raise ValueError("Errore nel parsing della pagina dell'episodio")
163
+ return source_match.group(1)
164
+
165
+ def _info_anime(self, anime: Anime):
166
+ html = self._get_html(anime.ref)
167
+
168
+ res = re.search(r'<a.*id="anilist-button".*href="\D*(\d*)"', html)
169
+ anilist_id = int(res.group(1)) if res else 0
170
+
171
+ temp = re.findall(
172
+ r'<dt>(.*?):</dt>[\n\s]*<dd(?: class="[^"]*")?>((?:.|\n)+?)</dd>', html
173
+ )
174
+ plot_match = re.search(r'<div class="desc">((?:.|\n)+?)</div>', html)
175
+ if plot_match:
176
+ temp.append(("Trama", plot_match.group(1)))
177
+
178
+ info = dict[str, str]()
179
+ status = AnimeStatus.UNKNOWN
180
+ for key, value in temp:
181
+ key, value = key.strip(), value.strip()
182
+ if key == "Stato":
183
+ if match := re.search(r'status=(\d+)"', value):
184
+ status_val = int(match.group(1))
185
+ if status_val < len(AnimeStatus):
186
+ status = list(AnimeStatus)[status_val]
187
+ value = re.sub(r"[\s\n]+", " ", re.sub(r"<.*?>", "", value)).strip()
188
+ info[key] = unescape(value)
189
+
190
+ anime.set_info(anilist_id, status, info)
191
+
192
+ def _parse_info(self, elem: dict) -> tuple[int, AnimeStatus, dict[str, str]]:
193
+ """Estrae e mappa i metadati ritornati dal JSON dell'API di ricerca di AnimeWorld."""
194
+ anilist_id = elem.get("anilistId", 0)
195
+ state_val = elem.get("stateName", "Sconosciuto")
196
+ status = AnimeStatus.UNKNOWN
197
+ if state_val == "Finito":
198
+ status = AnimeStatus.FINISHED
199
+ elif state_val == "In corso":
200
+ status = AnimeStatus.ONGOING
201
+ elif state_val == "Non rilasciato":
202
+ status = AnimeStatus.NOT_RELEASED
203
+
204
+ info = {
205
+ "Categoria": "Anime" if elem.get("animeType") == "0" else "Movie",
206
+ "Audio": "Italiano" if elem.get("dub") == "1" else "Giapponese",
207
+ "Data di Uscita": elem.get("release") or elem.get("year", ""),
208
+ "Studio": elem.get("studio", ""),
209
+ "Genere": ", ".join(
210
+ cat.get("name", "") for cat in elem.get("categories", [])
211
+ ),
212
+ "Voto": f"{elem.get('malVote')}" if elem.get("malVote") else "Sconosciuto",
213
+ "Episodi": f"{elem.get('episodes', '0')}",
214
+ "Stato": state_val,
215
+ "Trama": elem.get("story", ""),
216
+ }
217
+ return int(anilist_id) if anilist_id else 0, status, info
@@ -12,7 +12,7 @@ class LocalProvider(Provider):
12
12
  - info_anime: carica info dalla cronologia
13
13
  """
14
14
  def __init__(self, path, history: list[Anime]):
15
- super().__init__(path)
15
+ self.BASE_URL = path
16
16
  self.history = history
17
17
 
18
18
  def _search(self, input: str) -> list[Anime]:
@@ -1,5 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
- from httpx import Client, HTTPError #, AsyncClient
2
+ from curses import error
3
+ from httpx import Client, HTTPError # , AsyncClient
3
4
  from ..anime import Anime
4
5
  from .. import utilities as ut
5
6
 
@@ -8,6 +9,7 @@ def error_handler(relink=False):
8
9
  """
9
10
  Decoratore per gestire gli errori delle funzioni.
10
11
  """
12
+
11
13
  def decorator(func):
12
14
  def wrapper(self, *args, **kwargs):
13
15
  try:
@@ -15,11 +17,17 @@ def error_handler(relink=False):
15
17
  except Exception as e:
16
18
  if relink and isinstance(e, HTTPError):
17
19
  return update_link(self, func, *args, **kwargs)
18
- ut.console.print(f"Errore {e.__class__.__name__} durante {func.__name__}: {e}", style="error")
19
- return None
20
+ ut.console.print(
21
+ f"Errore {e.__class__.__name__} durante {func.__name__}: {e}",
22
+ style="error",
23
+ )
24
+ exit(1)
25
+
20
26
  return wrapper
27
+
21
28
  return decorator
22
29
 
30
+
23
31
  def update_link(self, callback, anime: Anime, episode: Anime.Episode | None = None):
24
32
  """
25
33
  Gestisce il caso in cui il riferimento dell'anime o all'episodio non è più valido
@@ -30,9 +38,11 @@ def update_link(self, callback, anime: Anime, episode: Anime.Episode | None = No
30
38
  return callback(self, anime, anime.episode(episode.num))
31
39
  else:
32
40
  ut.console.print("Il link dell'anime è stato cambiato", style="warning")
33
- res: list[Anime] = self.search(anime.name)
34
- if len(res) == 0:
35
- raise LookupError(f"Nessun risultato trovato per {anime.name} su {self.__class__.__name__}")
41
+ res: list[Anime] | None = self.search(anime.name)
42
+ if not res:
43
+ ut.console.print(f"Errore o nessun risultato durante la ricerca di {anime.name} su {self.__class__.__name__}", style="error")
44
+ ut.console.print("Cercarlo manualmente", style="highlight")
45
+ exit(1)
36
46
  anime.ref = res[0].ref
37
47
  return callback(self, anime)
38
48
 
@@ -45,20 +55,23 @@ class Provider(ABC):
45
55
  _url (str): l'url del sito del provider.
46
56
  _headers (dict): gli headers da utilizzare per le richieste HTTP.
47
57
  """
48
- def __init__(self, url: str):
58
+
59
+ def __init__(self, url: str, client: Client | None = None):
49
60
  """
50
61
  Costruisce un'instanza di Provider.
51
62
 
52
63
  Args:
53
64
  url (str): l'url del sito del provider.
65
+ client (Client | None): client HTTP opzionale da iniettare (utile per testing).
54
66
  """
55
67
  self.BASE_URL = url
56
- self.Client = Client(
68
+ self.Client = client or Client(
57
69
  headers={
58
- 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
70
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
71
+ "Connection": "close"
59
72
  },
60
73
  follow_redirects=True,
61
- timeout=10
74
+ timeout=10,
62
75
  )
63
76
 
64
77
  @error_handler(relink=False)
@@ -131,8 +144,7 @@ class Provider(ABC):
131
144
  anime (Anime): l'anime di riferimento.
132
145
  """
133
146
  anime.update_episodes(
134
- self._episodes(anime),
135
- ut.config_data["general"]["specials"]
147
+ self._episodes(anime), ut.config_data["general"]["specials"]
136
148
  )
137
149
 
138
150
  @abstractmethod