aw-cli 2.3.1__tar.gz → 2.3.3__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 (49) hide show
  1. aw_cli-2.3.3/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
  2. {aw_cli-2.3.1 → aw_cli-2.3.3}/.gitignore +3 -0
  3. {aw_cli-2.3.1 → aw_cli-2.3.3}/PKG-INFO +5 -3
  4. {aw_cli-2.3.1 → aw_cli-2.3.3}/pyproject.toml +9 -1
  5. aw_cli-2.3.3/src/aw_cli/download.py +188 -0
  6. {aw_cli-2.3.1/src/aw_cli/fzf → aw_cli-2.3.3/src/aw_cli/interface}/fzf.py +31 -10
  7. aw_cli-2.3.3/src/aw_cli/providers/__init__.py +36 -0
  8. {aw_cli-2.3.1 → aw_cli-2.3.3}/src/aw_cli/providers/animeunity.py +14 -14
  9. aw_cli-2.3.3/src/aw_cli/providers/animeworld.py +217 -0
  10. {aw_cli-2.3.1 → aw_cli-2.3.3}/src/aw_cli/providers/local.py +1 -1
  11. {aw_cli-2.3.1 → aw_cli-2.3.3}/src/aw_cli/providers/provider.py +8 -6
  12. {aw_cli-2.3.1 → aw_cli-2.3.3}/src/aw_cli/run.py +274 -119
  13. aw_cli-2.3.3/tests/conftest.py +128 -0
  14. aw_cli-2.3.3/tests/core/__init__.py +0 -0
  15. aw_cli-2.3.3/tests/core/test_download.py +53 -0
  16. aw_cli-2.3.3/tests/interface/__init__.py +0 -0
  17. {aw_cli-2.3.1/tests → aw_cli-2.3.3/tests/interface}/test_fzf.py +36 -5
  18. aw_cli-2.3.3/tests/providers/__init__.py +0 -0
  19. aw_cli-2.3.3/tests/providers/test_animeunity.py +90 -0
  20. aw_cli-2.3.3/tests/providers/test_animeworld.py +94 -0
  21. aw_cli-2.3.3/tests/providers/test_provider.py +42 -0
  22. aw_cli-2.3.3/uv.lock +761 -0
  23. aw_cli-2.3.1/src/aw_cli/download.py +0 -97
  24. aw_cli-2.3.1/src/aw_cli/providers/__init__.py +0 -12
  25. aw_cli-2.3.1/src/aw_cli/providers/animeworld.py +0 -123
  26. aw_cli-2.3.1/tests/empty +0 -25
  27. aw_cli-2.3.1/tests/search_dragonball +0 -216
  28. aw_cli-2.3.1/tests/search_onepiece +0 -127
  29. aw_cli-2.3.1/tests/theeminenceinshadowep13 +0 -499
  30. aw_cli-2.3.1/uv.lock +0 -773
  31. {aw_cli-2.3.1 → aw_cli-2.3.3}/.github/workflows/python-publish.yml +0 -0
  32. {aw_cli-2.3.1 → aw_cli-2.3.3}/.github/workflows/release.yml +0 -0
  33. {aw_cli-2.3.1 → aw_cli-2.3.3}/.pre-commit-config.yaml +0 -0
  34. {aw_cli-2.3.1 → aw_cli-2.3.3}/LICENSE +0 -0
  35. {aw_cli-2.3.1 → aw_cli-2.3.3}/README.md +0 -0
  36. {aw_cli-2.3.1 → aw_cli-2.3.3}/disclaimer.md +0 -0
  37. {aw_cli-2.3.1 → aw_cli-2.3.3}/src/aw_cli/__init__.py +0 -0
  38. {aw_cli-2.3.1 → aw_cli-2.3.3}/src/aw_cli/anilist.py +0 -0
  39. {aw_cli-2.3.1 → aw_cli-2.3.3}/src/aw_cli/anime.py +0 -0
  40. {aw_cli-2.3.1 → aw_cli-2.3.3}/src/aw_cli/arg_parser.py +0 -0
  41. {aw_cli-2.3.1 → aw_cli-2.3.3}/src/aw_cli/history.py +0 -0
  42. {aw_cli-2.3.1/src/aw_cli/fzf → aw_cli-2.3.3/src/aw_cli/interface}/__init__.py +0 -0
  43. {aw_cli-2.3.1 → aw_cli-2.3.3}/src/aw_cli/update.py +0 -0
  44. {aw_cli-2.3.1 → aw_cli-2.3.3}/src/aw_cli/utilities.py +0 -0
  45. {aw_cli-2.3.1 → aw_cli-2.3.3}/tests/__init__.py +0 -0
  46. {aw_cli-2.3.1/tests → aw_cli-2.3.3/tests/core}/test_anime.py +0 -0
  47. {aw_cli-2.3.1/tests → aw_cli-2.3.3/tests/core}/test_history.py +0 -0
  48. {aw_cli-2.3.1/tests → aw_cli-2.3.3/tests/core}/test_run.py +0 -0
  49. {aw_cli-2.3.1/tests → aw_cli-2.3.3/tests/interface}/test_rich_integration.py +0 -0
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: Bug report
3
+ about: Segnala un problema con aw-cli
4
+ title: "[Bug]: "
5
+ labels: bug
6
+ assignees: ""
7
+ ---
8
+
9
+ ## Descrizione del problema
10
+
11
+ Descrivi chiaramente il problema riscontrato.
12
+
13
+ ## Passi per riprodurre
14
+
15
+ Esempio:
16
+
17
+ 1. Esegui `aw-cli ...`
18
+ 2. Seleziona ...
19
+ 3. Il problema si verifica quando ...
20
+
21
+ ## Informazioni sull'ambiente
22
+
23
+ ### Versione di aw-cli
24
+
25
+ Esegui:
26
+
27
+ ```bash
28
+ aw-cli -v
29
+ ```
30
+
31
+ Output:
32
+
33
+ ```text
34
+ ...
35
+ ```
36
+
37
+ ### Sistema operativo
38
+
39
+ Esempio: Fedora 40, Ubuntu 24.04, Arch Linux, macOS, Android, Windows WSL, ecc.
40
+
41
+ ```text
42
+ ...
43
+ ```
44
+
45
+ ### Metodo di installazione
46
+
47
+ - [ ] `uv`
48
+ - [ ] `pipx`
49
+ - [ ] `pip`
50
+ - [ ] Installazione manuale / da sorgente
51
+ - [ ] Altro (specifica qui)
52
+
53
+ ### Provider
54
+
55
+ - [ ] AnimeWorld
56
+ - [ ] AnimeUnity
57
+
58
+ ### Player
59
+
60
+ - [ ] mpv
61
+ - [ ] vlc
62
+
63
+ ## Log / output del terminale
64
+
65
+ Incolla qui l'output completo dell'errore, se presente.
66
+
67
+ ```
68
+ ...
69
+ ```
70
+
71
+ ## Informazioni aggiuntive
72
+
73
+ 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,17 +1,19 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: aw-cli
3
- Version: 2.3.1
3
+ Version: 2.3.3
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
8
10
  Requires-Dist: httpx>=0.28.1
9
11
  Requires-Dist: pip-system-certs>=5.2
12
+ Requires-Dist: pip>=26.2.0
10
13
  Requires-Dist: pydantic>=2.12.5
11
14
  Requires-Dist: regex>=2025.9.18
12
15
  Requires-Dist: rich>=13.9.4
13
16
  Requires-Dist: toml>=0.10.2
14
- Requires-Dist: wheel>=0.45.1
15
17
  Description-Content-Type: text/markdown
16
18
 
17
19
  # aw-cli
@@ -7,14 +7,18 @@ license = "GPL-3.0"
7
7
  requires-python = ">=3.10"
8
8
  dependencies = [
9
9
  "httpx>=0.28.1",
10
+ "pip>=26.2.0",
10
11
  "pip-system-certs>=5.2",
11
12
  "pydantic>=2.12.5",
12
13
  "regex>=2025.9.18",
13
14
  "rich>=13.9.4",
14
15
  "toml>=0.10.2",
15
- "wheel>=0.45.1",
16
16
  ]
17
17
 
18
+ [project.urls]
19
+ Homepage = "https://github.com/fexh10/aw-cli"
20
+ Repository = "https://github.com/fexh10/aw-cli"
21
+
18
22
  [project.scripts]
19
23
  aw-cli = "aw_cli.run:main"
20
24
 
@@ -31,3 +35,7 @@ dev = [
31
35
  "pyright>=1.1.406",
32
36
  "pytest>=8.4.2",
33
37
  ]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ addopts = "-ra"
@@ -0,0 +1,188 @@
1
+ import asyncio
2
+ import re
3
+ from pathlib import Path
4
+ from httpx import AsyncClient
5
+ from rich.progress import Progress, BarColumn, TextColumn, TaskID, DownloadColumn, TransferSpeedColumn
6
+ from . import utilities as ut
7
+ from .anime import Anime
8
+ from .providers import Provider
9
+
10
+ from functools import lru_cache
11
+
12
+ # Numero di connessioni parallele usate di default per scaricare un singolo
13
+ # episodio. Molti CDN limitano la banda per-connessione: scaricare lo stesso
14
+ # file a segmenti con più connessioni aggira quel limite. Sovrascrivibile
15
+ # tramite l'opzione "connections-per-download" nel file di configurazione.
16
+ DEFAULT_CONNECTIONS = 4
17
+
18
+ # Dimensione dei blocchi letti dallo stream. 1 MB riduce drasticamente
19
+ # l'overhead rispetto a letture da 1 KB su file di centinaia di MB.
20
+ CHUNK_SIZE = 1024 * 1024
21
+
22
+ def _segment_ranges(total: int, connections: int) -> list[tuple[int, int]]:
23
+ """
24
+ Divide un download in intervalli byte inclusivi, evitando segmenti vuoti.
25
+ """
26
+ if total <= 0 or connections <= 0:
27
+ return []
28
+
29
+ connections = min(connections, total)
30
+ base_size, remainder = divmod(total, connections)
31
+ ranges = []
32
+ start = 0
33
+ for i in range(connections):
34
+ size = base_size + (1 if i < remainder else 0)
35
+ end = start + size - 1
36
+ ranges.append((start, end))
37
+ start = end + 1
38
+ return ranges
39
+
40
+ def _valid_partial_response(response, start: int, end: int, total: int) -> bool:
41
+ """
42
+ Verifica che il server abbia rispettato il Range richiesto.
43
+ """
44
+ if response.status_code != 206:
45
+ return False
46
+
47
+ content_range = response.headers.get("content-range", "")
48
+ match = re.fullmatch(r"bytes (\d+)-(\d+)/(\d+|\*)", content_range)
49
+ if not match:
50
+ return False
51
+
52
+ actual_start = int(match.group(1))
53
+ actual_end = int(match.group(2))
54
+ actual_total = match.group(3)
55
+ return (
56
+ actual_start == start
57
+ and actual_end == end
58
+ and (actual_total == "*" or int(actual_total) == total)
59
+ )
60
+
61
+ @lru_cache
62
+ def path(create: bool = True) -> Path:
63
+ """
64
+ Restituisce il percorso di download dell'anime, a seconda del sistema operativo in uso.
65
+ Se create è True (valore predefinito) e il percorso non esiste, viene creato.
66
+
67
+ Args:
68
+ create (bool, optional): se impostato a True, crea il percorso se non esiste. Valore predefinito: True.
69
+
70
+ Returns:
71
+ Path: il percorso di download dell'anime.
72
+ """
73
+
74
+ if (ut.os_name == "Android"):
75
+ path = Path("/sdcard/Movies/Anime")
76
+ else:
77
+ path = Path.home() / "Videos/Anime"
78
+
79
+ if create:
80
+ path.mkdir(parents=True, exist_ok=True)
81
+ return path
82
+
83
+ def episodes(anime: Anime, episodes: list[Anime.Episode], provider: Provider) -> None:
84
+ """
85
+ Scarica più episodi in modo concorrente (max concurrent_downloads).
86
+
87
+ Args:
88
+ anime (Anime): l'anime di cui scaricare gli episodi.
89
+ episodes (list[Anime.Episode]): la lista degli episodi da scaricare.
90
+ provider (Provider): il provider da cui scaricare gli episodi.
91
+ """
92
+ async def download_worker(ep: Anime.Episode, task_id: TaskID, progress: Progress, sem: asyncio.Semaphore):
93
+ async with sem:
94
+ filename = path() / anime.name / f"{ep}.mp4"
95
+ if filename.exists():
96
+ progress.update(task_id, completed=100, total=100, description=f"[green]Ep. {ep.num} (Esistente)")
97
+ return
98
+
99
+ filename.parent.mkdir(parents=True, exist_ok=True)
100
+ try:
101
+ url = provider.episode_link(anime, ep)
102
+ except Exception as e:
103
+ progress.console.print(f"[red]Errore link Ep. {ep.num}: {e}")
104
+ return
105
+
106
+ headers = provider.Client.headers
107
+ temp_filename = filename.with_name(f"{filename.name}.temp")
108
+ connections = max(1, ut.config_data["general"].get("connections-per-download", DEFAULT_CONNECTIONS))
109
+
110
+ async def single_stream(client: AsyncClient) -> None:
111
+ """Scarica l'episodio con un'unica connessione (fallback)."""
112
+ async with client.stream("GET", url, headers=headers) as response:
113
+ response.raise_for_status()
114
+ total = int(response.headers.get('content-length', 0))
115
+ progress.update(task_id, total=total or None)
116
+ with open(temp_filename, "wb") as f:
117
+ async for chunk in response.aiter_bytes(CHUNK_SIZE):
118
+ if chunk:
119
+ progress.update(task_id, advance=f.write(chunk))
120
+
121
+ async def download_segment(client: AsyncClient, start: int, end: int) -> None:
122
+ """Scarica l'intervallo di byte [start, end] e lo scrive alla sua posizione nel file."""
123
+ seg_headers = {**headers, "Range": f"bytes={start}-{end}"}
124
+ async with client.stream("GET", url, headers=seg_headers) as response:
125
+ if not _valid_partial_response(response, start, end, total):
126
+ raise ValueError(
127
+ f"Risposta Range non valida per bytes={start}-{end}"
128
+ )
129
+ with open(temp_filename, "r+b") as f:
130
+ f.seek(start)
131
+ async for chunk in response.aiter_bytes(CHUNK_SIZE):
132
+ if chunk:
133
+ progress.update(task_id, advance=f.write(chunk))
134
+
135
+ try:
136
+ async with AsyncClient(timeout=30.0, follow_redirects=True) as client:
137
+ total = 0
138
+ accept_ranges = False
139
+ try:
140
+ head = await client.head(url, headers=headers)
141
+ head.raise_for_status()
142
+ total = int(head.headers.get('content-length', 0))
143
+ accept_ranges = head.headers.get('accept-ranges', '').lower() == 'bytes'
144
+ except Exception:
145
+ pass
146
+
147
+ if connections > 1 and accept_ranges and total > 0:
148
+ # Download segmentato: più connessioni in parallelo sullo stesso
149
+ # file, per aggirare i limiti di banda per-connessione dei CDN.
150
+ progress.update(task_id, total=total)
151
+ with open(temp_filename, "wb") as f:
152
+ f.truncate(total)
153
+ segments = [
154
+ download_segment(client, start, end)
155
+ for start, end in _segment_ranges(total, connections)
156
+ ]
157
+ await asyncio.gather(*segments)
158
+ else:
159
+ # Server senza supporto ai range (o dimensione ignota): fallback.
160
+ await single_stream(client)
161
+
162
+ temp_filename.rename(filename)
163
+ progress.update(task_id, description=f"[success]Ep. {ep.num} (Completato)[/]")
164
+ except Exception as e:
165
+ progress.console.print(f"[error]Errore download Ep. {ep.num}: {e}[/]")
166
+
167
+ async def _download_all():
168
+ concurrent_downloads = ut.config_data["general"].get("parallel-downloads", 3)
169
+ semaphore = asyncio.Semaphore(concurrent_downloads)
170
+ ordered_eps = sorted(episodes, key=lambda e: e.numeric())
171
+
172
+ with Progress(
173
+ TextColumn("[info]{task.description}[/]", style="info"),
174
+ BarColumn(bar_width=None),
175
+ "•",
176
+ DownloadColumn(),
177
+ "•",
178
+ TransferSpeedColumn(),
179
+ console=ut.console,
180
+ ) as progress:
181
+ tasks = []
182
+ for ep in ordered_eps:
183
+ task_id = progress.add_task(str(ep), total=None, start=True)
184
+ tasks.append(asyncio.create_task(download_worker(ep, task_id, progress, semaphore)))
185
+
186
+ await asyncio.gather(*tasks)
187
+
188
+ asyncio.run(_download_all())
@@ -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":