aw-cli 2.2__tar.gz → 2.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 (39) hide show
  1. {aw_cli-2.2 → aw_cli-2.3}/.github/workflows/python-publish.yml +32 -32
  2. aw_cli-2.3/.github/workflows/release.yml +19 -0
  3. {aw_cli-2.2 → aw_cli-2.3}/PKG-INFO +3 -1
  4. {aw_cli-2.2 → aw_cli-2.3}/pyproject.toml +2 -0
  5. {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/anilist.py +28 -26
  6. {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/anime.py +63 -26
  7. {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/arg_parser.py +19 -19
  8. aw_cli-2.3/src/aw_cli/download.py +97 -0
  9. aw_cli-2.3/src/aw_cli/fzf/__init__.py +3 -0
  10. aw_cli-2.3/src/aw_cli/fzf/fzf.py +192 -0
  11. {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/history.py +29 -25
  12. {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/providers/animeunity.py +3 -3
  13. {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/providers/animeworld.py +3 -3
  14. {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/providers/local.py +15 -6
  15. {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/providers/provider.py +32 -13
  16. aw_cli-2.3/src/aw_cli/run.py +589 -0
  17. {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/update.py +5 -4
  18. aw_cli-2.3/src/aw_cli/utilities.py +87 -0
  19. aw_cli-2.3/tests/test_fzf.py +97 -0
  20. {aw_cli-2.2 → aw_cli-2.3}/tests/test_history.py +3 -3
  21. aw_cli-2.3/tests/test_rich_integration.py +27 -0
  22. aw_cli-2.3/tests/test_run.py +34 -0
  23. {aw_cli-2.2 → aw_cli-2.3}/uv.lock +192 -0
  24. aw_cli-2.2/src/aw_cli/download.py +0 -111
  25. aw_cli-2.2/src/aw_cli/run.py +0 -560
  26. aw_cli-2.2/src/aw_cli/utilities.py +0 -107
  27. {aw_cli-2.2 → aw_cli-2.3}/.gitignore +0 -0
  28. {aw_cli-2.2 → aw_cli-2.3}/.pre-commit-config.yaml +0 -0
  29. {aw_cli-2.2 → aw_cli-2.3}/LICENSE +0 -0
  30. {aw_cli-2.2 → aw_cli-2.3}/README.md +0 -0
  31. {aw_cli-2.2 → aw_cli-2.3}/disclaimer.md +0 -0
  32. {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/__init__.py +0 -0
  33. {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/providers/__init__.py +0 -0
  34. {aw_cli-2.2 → aw_cli-2.3}/tests/__init__.py +0 -0
  35. {aw_cli-2.2 → aw_cli-2.3}/tests/empty +0 -0
  36. {aw_cli-2.2 → aw_cli-2.3}/tests/search_dragonball +0 -0
  37. {aw_cli-2.2 → aw_cli-2.3}/tests/search_onepiece +0 -0
  38. {aw_cli-2.2 → aw_cli-2.3}/tests/test_anime.py +0 -0
  39. {aw_cli-2.2 → aw_cli-2.3}/tests/theeminenceinshadowep13 +0 -0
@@ -1,32 +1,32 @@
1
- name: "Upload python package"
2
-
3
- on:
4
- push:
5
- tags:
6
- # Publish on any tag starting with a `v`, e.g., v0.1.0
7
- - v*
8
-
9
- jobs:
10
- run:
11
- runs-on: ubuntu-latest
12
- environment:
13
- name: pypi
14
- permissions:
15
- id-token: write
16
- contents: read
17
- steps:
18
- - name: Checkout
19
- uses: actions/checkout@v5
20
- - name: Install uv
21
- uses: astral-sh/setup-uv@v6
22
- - name: Install Python 3.13
23
- run: uv python install 3.13
24
- - name: Build
25
- run: uv build
26
- # Check that basic features work and we didn't miss to include crucial files
27
- #- name: Smoke test (wheel)
28
- # run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
29
- #- name: Smoke test (source distribution)
30
- # run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
31
- - name: Publish
32
- run: uv publish
1
+ name: "Upload python package"
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ # Publish on any tag starting with a `v`, e.g., v0.1.0
7
+ - v*
8
+
9
+ jobs:
10
+ run:
11
+ runs-on: ubuntu-latest
12
+ environment:
13
+ name: pypi
14
+ permissions:
15
+ id-token: write
16
+ contents: read
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v5
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v6
22
+ - name: Install Python 3.13
23
+ run: uv python install 3.13
24
+ - name: Build
25
+ run: uv build
26
+ # Check that basic features work and we didn't miss to include crucial files
27
+ #- name: Smoke test (wheel)
28
+ # run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
29
+ #- name: Smoke test (source distribution)
30
+ # run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
31
+ - name: Publish
32
+ run: uv publish
@@ -0,0 +1,19 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: softprops/action-gh-release@v2
16
+ with:
17
+ generate_release_notes: true
18
+ env:
19
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,13 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aw-cli
3
- Version: 2.2
3
+ Version: 2.3
4
4
  Summary: Guarda anime dal terminale e molto altro!
5
5
  License-Expression: GPL-3.0
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.10
8
8
  Requires-Dist: httpx>=0.28.1
9
9
  Requires-Dist: pip-system-certs>=5.2
10
+ Requires-Dist: pydantic>=2.12.5
10
11
  Requires-Dist: regex>=2025.9.18
12
+ Requires-Dist: rich>=13.9.4
11
13
  Requires-Dist: toml>=0.10.2
12
14
  Requires-Dist: wheel>=0.45.1
13
15
  Description-Content-Type: text/markdown
@@ -8,7 +8,9 @@ requires-python = ">=3.10"
8
8
  dependencies = [
9
9
  "httpx>=0.28.1",
10
10
  "pip-system-certs>=5.2",
11
+ "pydantic>=2.12.5",
11
12
  "regex>=2025.9.18",
13
+ "rich>=13.9.4",
12
14
  "toml>=0.10.2",
13
15
  "wheel>=0.45.1",
14
16
  ]
@@ -3,25 +3,25 @@ import httpx
3
3
  class TokenError(Exception):
4
4
  pass
5
5
 
6
- def updateAnilist(token, id_anilist: int, ep: int, status_list: str, score: float, favourite: bool = False) -> None:
6
+ def update_anilist(token: str, anilist_id: int, ep: int, status_list: str, score: float, favourite: bool = False) -> None:
7
7
  """
8
8
  Collegamento alle API di AniList per aggiornare lo stato dell'anime
9
9
 
10
10
  Args:
11
- id_anilist (int): l'id dell'anime su AniList.
11
+ anilist_id (int): l'id dell'anime su AniList.
12
12
  ep (int): il numero dell'episodio visualizzato.
13
13
  status_list (str): lo stato dell'anime per l'utente. Se è in corso verrà impostato su "CURRENT", se completato su "COMPLETED".
14
14
  score (float): il voto dell'anime.
15
15
  favourite (bool): se True, aggiunge l'anime ai preferiti.
16
16
  """
17
17
  query = """
18
- mutation ($idAnime: Int, $status: MediaListStatus, $episode : Int, $score: Float) {
19
- SaveMediaListEntry (mediaId: $idAnime, status: $status, progress : $episode, score: $score) {
18
+ mutation ($anime_id: Int, $status: MediaListStatus, $episode : Int, $score: Float) {
19
+ SaveMediaListEntry (mediaId: $anime_id, status: $status, progress : $episode, score: $score) {
20
20
  status
21
21
  progress
22
22
  score
23
23
  },""" + ("""
24
- ToggleFavourite(animeId:$idAnime){
24
+ ToggleFavourite(animeId:$anime_id){
25
25
  anime {
26
26
  nodes {
27
27
  id
@@ -32,7 +32,7 @@ def updateAnilist(token, id_anilist: int, ep: int, status_list: str, score: floa
32
32
  """
33
33
 
34
34
  var = {
35
- "idAnime": id_anilist,
35
+ "anime_id": anilist_id,
36
36
  "status": status_list,
37
37
  "episode": ep,
38
38
  }
@@ -40,14 +40,16 @@ def updateAnilist(token, id_anilist: int, ep: int, status_list: str, score: floa
40
40
  if score != 0:
41
41
  var["score"] = score
42
42
 
43
- requestModifyAnilist(token, query, var)
43
+ make_anilist_request(token, query, var)
44
44
 
45
-
46
- def getAnilistUserId(token) -> int:
45
+ def get_user_id(token: str) -> int:
47
46
  """
48
47
  Collegamento alle API di AniList per trovare
49
48
  l'id dell'utente in base al token AniList dell'utente.
50
49
 
50
+ Args:
51
+ token (str): il token AniList dell'utente.
52
+
51
53
  Returns:
52
54
  int: l'id dell'utente.
53
55
  """
@@ -60,58 +62,58 @@ def getAnilistUserId(token) -> int:
60
62
  }
61
63
  """
62
64
 
63
- header_anilist = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', 'Accept': 'application/json'}
64
- risposta = httpx.post('https://graphql.anilist.co', headers=header_anilist, json={'query': query})
65
- if risposta.status_code != 200:
65
+ headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', 'Accept': 'application/json'}
66
+ response = httpx.post('https://graphql.anilist.co', headers=headers, json={'query': query})
67
+ if response.status_code != 200:
66
68
  raise TokenError("Errore: Token AniList sbagliato")
67
69
 
68
- user_id = int(risposta.json()["data"]["Viewer"]["id"])
70
+ user_id = int(response.json()["data"]["Viewer"]["id"])
69
71
 
70
72
  return user_id
71
73
 
72
-
73
- def getAnimePrivateRating(token, user_id, id_anime: int) -> (float | None):
74
+ def get_anime_private_rating(token, user_id, anime_id: int) -> (float | None):
74
75
  """
75
76
  Collegamento alle API di AniList per trovare
76
77
  il voto dato all'anime dall'utente.
77
78
 
78
79
  Args:
79
- id_anime (int): l'id dell'anime su Anilist.
80
+ user_id (int): l'id dell'utente.
81
+ anime_id (int): l'id dell'anime su Anilist.
80
82
 
81
83
  Returns:
82
84
  float: il voto dell'utente.
83
85
  """
84
86
 
85
87
  query = """
86
- query ($idAnime: Int, $userId: Int) {
87
- MediaList(userId: $userId, mediaId: $idAnime) {
88
+ query ($anime_id: Int, $user_id: Int) {
89
+ MediaList(userId: $user_id, mediaId: $anime_id) {
88
90
  score
89
91
  }
90
92
  }
91
93
  """
92
94
  var = {
93
- "idAnime": id_anime,
94
- "userId": user_id
95
+ "anime_id": anime_id,
96
+ "user_id": user_id
95
97
  }
96
98
 
97
- header_anilist = {
99
+ headers = {
98
100
  'Authorization': f'Bearer {token}',
99
101
  'Content-Type': 'application/json', 'Accept': 'application/json'
100
102
  }
101
103
 
102
- risposta = httpx.post('https://graphql.anilist.co', headers=header_anilist, json={'query': query, 'variables': var})
103
- if risposta.status_code != 200:
104
+ response = httpx.post('https://graphql.anilist.co', headers=headers, json={'query': query, 'variables': var})
105
+ if response.status_code != 200:
104
106
  return None
105
107
 
106
- return float(risposta.json()["data"]["MediaList"]["score"])
107
-
108
+ return float(response.json()["data"]["MediaList"]["score"])
108
109
 
109
- def requestModifyAnilist(token, query: str, var: dict):
110
+ def make_anilist_request(token: str, query: str, var: dict) -> None:
110
111
  """
111
112
  Request alle API di Anilist.
112
113
  Se la richiesta non va a buon fine, viene stampato un errore.
113
114
 
114
115
  Args:
116
+ token (str): il token AniList dell'utente.
115
117
  query (str): la stringa che contiene la query.
116
118
  var (dict): dizionario che contiene le variabili da passare alla query.
117
119
  """
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
  from enum import Enum
3
3
  from functools import total_ordering
4
- from . import utilities as ut
4
+ from rich.console import Console, ConsoleOptions, RenderResult
5
+ from rich.text import Text
5
6
 
6
7
 
7
8
  class AnimeStatus(Enum):
@@ -30,16 +31,18 @@ class Anime:
30
31
  Attributes:
31
32
  name (str): il nome dell'anime.
32
33
  ref (str): il riferimento dell'anime.
34
+ anilist_id (int): l'ID dell'anime su Anilist.
35
+ dub (bool): indica se l'anime è doppiato in italiano.
33
36
  curr_ep (str): l'episodio corrente.
34
37
  last_ep (str): l'ultimo episodio disponibile.
35
- id_anilist (int): l'ID dell'anime su Anilist.
38
+ status (AnimeStatus): lo stato dell'anime.
36
39
  info (dict[str, str]): dizionario con le informazioni dell'anime.
37
40
  """
38
41
 
39
42
  def __init__(self, name: str, ref: str, curr_ep: str = "0", last_ep: str = "0", status: AnimeStatus = AnimeStatus.UNKNOWN) -> None:
40
43
  self.name: str = name
41
44
  self.ref: str = ref
42
- self.id_anilist: int = 0
45
+ self.anilist_id: int = 0
43
46
  self.dub: bool = "(ITA)" in name
44
47
  self.curr_ep: str = curr_ep
45
48
  self.last_ep: str = last_ep if last_ep != "0" else curr_ep
@@ -52,14 +55,14 @@ class Anime:
52
55
  if not isinstance(other, Anime):
53
56
  return False
54
57
 
55
- if self.id_anilist and other.id_anilist:
56
- return self.id_anilist == other.id_anilist and self.dub == other.dub
58
+ if self.anilist_id and other.anilist_id:
59
+ return self.anilist_id == other.anilist_id and self.dub == other.dub
57
60
 
58
61
  return self.name == other.name # idealmente andrebbe tolto
59
62
 
60
63
  def __hash__(self) -> int:
61
- if self.id_anilist:
62
- return hash((self.id_anilist, self.dub))
64
+ if self.anilist_id:
65
+ return hash((self.anilist_id, self.dub))
63
66
 
64
67
  return hash(self.name)
65
68
 
@@ -67,7 +70,7 @@ class Anime:
67
70
  """
68
71
  Imposta i riferimenti degli episodi dell'anime.
69
72
  Args:
70
- episodi (dict[str, str]): dizionario dei riferimenti degli episodi dell'anime (numero->URL/ID).
73
+ episodes (dict[str, str]): dizionario dei riferimenti degli episodi dell'anime (numero->URL/ID).
71
74
  """
72
75
  for num, ref in episodes.items():
73
76
  if not specials and ("." in num or num == "0"):
@@ -86,6 +89,9 @@ class Anime:
86
89
  def episodes(self) -> list[str]:
87
90
  """
88
91
  Restituisce una lista dei numeri degli episodi disponibili.
92
+
93
+ Returns:
94
+ list[str]: la lista dei numeri degli episodi disponibili.
89
95
  """
90
96
  return list(self._num_to_index.keys())
91
97
 
@@ -93,7 +99,7 @@ class Anime:
93
99
  """
94
100
  Controlla se l'anime ha l'episodio specificato.
95
101
  Args:
96
- ep (str): Il numero dell'episodio.
102
+ ep_num (str): Il numero dell'episodio.
97
103
  Returns:
98
104
  (bool): True se l'anime ha l'episodio, False altrimenti.
99
105
  """
@@ -103,7 +109,7 @@ class Anime:
103
109
  """
104
110
  Restituisce il riferimento dell'episodio corrispondente al numero specificato.
105
111
  Args:
106
- ep (str): Il numero dell'episodio.
112
+ ep_num (str): Il numero dell'episodio.
107
113
  Returns:
108
114
  Anime.Episode : l'episodio corrispondente al numero
109
115
  """
@@ -115,28 +121,59 @@ class Anime:
115
121
  Imposta le informazioni dell'anime.
116
122
  Args:
117
123
  anilist_id (int): l'ID di Anilist dell'anime.
118
- infos (list): lista delle informazioni dell'anime.
124
+ status (AnimeStatus): lo stato dell'anime.
125
+ info (dict): dizionario delle informazioni dell'anime.
119
126
  """
120
- self.id_anilist = anilist_id
127
+ self.anilist_id = anilist_id
121
128
  self.info = info
122
129
  self.status = status
123
130
  self.dub = info.get("Audio", "").lower() == "italiano"
124
131
 
125
- def print_info(self):
132
+ def effective_episodes_count(self) -> int:
126
133
  """
127
- Stampa le informazioni dell'anime.
134
+ Calcola il numero totale effettivo di episodi, ignorando gli speciali (.5)
135
+ e scompattando i batch (es. 1-12 conta come 12 episodi).
128
136
  """
129
- ut.my_print(self.name, cls=True)
130
- tmp_info = dict(self.info)
137
+ total = 0
138
+ for ep in self.episodes():
139
+ if '.' in ep:
140
+ continue
141
+ if '-' in ep:
142
+ parts = ep.split('-')
143
+ if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
144
+ total += (int(parts[1]) - int(parts[0]) + 1)
145
+ else:
146
+ total += 1
147
+ else:
148
+ total += 1
149
+ return total
150
+
151
+ def has_all_episodes(self) -> bool:
152
+ """
153
+ Verifica se l'anime ha già in memoria tutti gli episodi noti per essere stati rilasciati.
154
+ """
155
+ try:
156
+ return self.effective_episodes_count() >= int(self.last_ep)
157
+ except ValueError:
158
+ return False
131
159
 
132
- # Usa l'enum per lo stato
160
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
161
+ """
162
+ Implementazione del protocollo Rich per il rendering della classe.
163
+ """
164
+ yield Text(self.name, style="info")
165
+
166
+ tmp_info = dict(self.info)
133
167
  tmp_info["Stato"] = self.status.value
134
168
 
135
- tmp_info["Trama"] = tmp_info.pop("Trama")
169
+ if "Trama" in tmp_info:
170
+ tmp_info["Trama"] = tmp_info.pop("Trama")
136
171
 
137
172
  for key, value in tmp_info.items():
138
- ut.my_print(f"{key}: ", end="", color="azzurro")
139
- ut.my_print(value, format=0)
173
+ text = Text()
174
+ text.append(f"{key}: ", style="highlight")
175
+ text.append(str(value), style="general")
176
+ yield text
140
177
 
141
178
  @classmethod
142
179
  def from_dict(cls, data: dict[str, object]) -> Anime:
@@ -166,9 +203,8 @@ class Anime:
166
203
  )
167
204
 
168
205
  episodes_data = data["episodes"] if isinstance(data["episodes"], list) else []
169
- for ep_data in episodes_data:
170
- anime._episodes = [Anime.Episode.from_dict(anime, ep_data) for ep_data in episodes_data]
171
- anime.update_episodes()
206
+ anime._episodes = [Anime.Episode.from_dict(anime, ep) for ep in episodes_data]
207
+ anime.update_episodes()
172
208
 
173
209
  return anime
174
210
 
@@ -184,7 +220,7 @@ class Anime:
184
220
  "ref": self.ref,
185
221
  "curr_ep": self.curr_ep,
186
222
  "last_ep": self.last_ep,
187
- "id_anilist": self.id_anilist,
223
+ "id_anilist": self.anilist_id,
188
224
  "status": self.status.value,
189
225
  "info": self.info,
190
226
  "episodes": [ep.to_dict() for ep in self._episodes]
@@ -202,8 +238,6 @@ class Anime:
202
238
  completed (bool): indica se l'episodio è stato completato.
203
239
  """
204
240
 
205
- ## TODO: fare un comparatore in base a num (considerando i casi x, x.5, x-y)
206
-
207
241
  def __init__(self, anime: Anime, num: str, ref: str) -> None:
208
242
  self._anime = anime
209
243
  self.num = num
@@ -335,6 +369,9 @@ class Anime:
335
369
  def to_dict(self) -> dict[str, object]:
336
370
  """
337
371
  Restituisce un dizionario con le informazioni dell'episodio.
372
+
373
+ Returns:
374
+ dict: il dizionario con le informazioni dell'episodio.
338
375
  """
339
376
  return {
340
377
  "num": self.num,
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
  import argparse
3
- from .utilities import nome_os
3
+ from .utilities import os_name
4
4
  from importlib.metadata import version
5
5
  from .update import update
6
6
 
@@ -11,11 +11,11 @@ class Update(argparse.Action):
11
11
 
12
12
  # args
13
13
  downl = False
14
- lista = False
14
+ latest = False
15
15
  offline = False
16
- cronologia = False
16
+ hist = False
17
17
  info = False
18
- privato = False
18
+ private = False
19
19
 
20
20
  # args
21
21
  parser = argparse.ArgumentParser(
@@ -46,7 +46,7 @@ options_group.add_argument(
46
46
  '-c',
47
47
  '--cronologia',
48
48
  nargs='?',
49
- choices=['r'], dest='cronologia',
49
+ choices=['r'], dest='history',
50
50
  help='continua a guardare un anime dalla cronologia. \'r\' per rimuovere un anime (opzionale)'
51
51
  )
52
52
 
@@ -55,7 +55,7 @@ options_group.add_argument(
55
55
  '--lista',
56
56
  nargs='?',
57
57
  choices=['a', 's', 'd', 't'],
58
- dest='lista',
58
+ dest='latest',
59
59
  help="lista degli ultimi anime usciti. Filtri: a = all, s = sub, d = dub, t = tendenze. Default 'a'"
60
60
  )
61
61
 
@@ -67,7 +67,7 @@ options_group.add_argument(
67
67
  help='visualizza le informazioni e la trama di un anime'
68
68
  )
69
69
 
70
- if nome_os != "Android":
70
+ if os_name != "Android":
71
71
  options_group.add_argument(
72
72
  '-s',
73
73
  '--syncplay',
@@ -95,7 +95,7 @@ options_group.add_argument(
95
95
  '-p',
96
96
  '--privato',
97
97
  action='store_true',
98
- dest='privato',
98
+ dest='private',
99
99
  help="guarda un episodio senza che si aggiorni la cronologia o AniList"
100
100
  )
101
101
 
@@ -112,7 +112,7 @@ config_group.add_argument(
112
112
  '-a',
113
113
  '--configurazione',
114
114
  action='store_true',
115
- dest='avvia_config',
115
+ dest='start_config',
116
116
  help='avvia il menu di configurazione'
117
117
  )
118
118
 
@@ -120,19 +120,19 @@ args = parser.parse_args()
120
120
 
121
121
  if args.offline:
122
122
  offline = True
123
- cronologia = True
124
- elif args.cronologia == 'r':
125
- cronologia = True
123
+ hist = True
124
+ elif args.history == 'r':
125
+ hist = True
126
126
  else:
127
127
  if args.info:
128
128
  info = True
129
129
  if args.download:
130
130
  downl = True
131
- if args.lista or '-l' in sys.argv:
132
- if args.lista is None:
133
- args.lista = 'a'
134
- lista = True
135
- if args.privato:
136
- privato = True
131
+ if args.latest or '-l' in sys.argv:
132
+ if args.latest is None:
133
+ args.latest = 'a'
134
+ latest = True
135
+ if args.private:
136
+ private = True
137
137
  if '-c' in sys.argv:
138
- cronologia = True
138
+ hist = True
@@ -0,0 +1,97 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+ from httpx import AsyncClient
4
+ from rich.progress import Progress, BarColumn, TextColumn, TaskID, DownloadColumn, TransferSpeedColumn
5
+ from . import utilities as ut
6
+ from .anime import Anime
7
+ from .providers import Provider
8
+
9
+ from functools import lru_cache
10
+
11
+ @lru_cache
12
+ def path(create: bool = True) -> Path:
13
+ """
14
+ Restituisce il percorso di download dell'anime, a seconda del sistema operativo in uso.
15
+ Se create è True (valore predefinito) e il percorso non esiste, viene creato.
16
+
17
+ Args:
18
+ create (bool, optional): se impostato a True, crea il percorso se non esiste. Valore predefinito: True.
19
+
20
+ Returns:
21
+ Path: il percorso di download dell'anime.
22
+ """
23
+
24
+ if (ut.os_name == "Android"):
25
+ path = Path("/sdcard/Movies/Anime")
26
+ else:
27
+ path = Path.home() / "Videos/Anime"
28
+
29
+ if create:
30
+ path.mkdir(parents=True, exist_ok=True)
31
+ return path
32
+
33
+ def episodes(anime: Anime, episodes: list[Anime.Episode], provider: Provider) -> None:
34
+ """
35
+ Scarica più episodi in modo concorrente (max concurrent_downloads).
36
+
37
+ Args:
38
+ anime (Anime): l'anime di cui scaricare gli episodi.
39
+ episodes (list[Anime.Episode]): la lista degli episodi da scaricare.
40
+ provider (Provider): il provider da cui scaricare gli episodi.
41
+ """
42
+ async def download_worker(ep: Anime.Episode, task_id: TaskID, progress: Progress, sem: asyncio.Semaphore):
43
+ async with sem:
44
+ filename = path() / anime.name / f"{ep}.mp4"
45
+ if filename.exists():
46
+ progress.update(task_id, completed=100, total=100, description=f"[green]Ep. {ep.num} (Esistente)")
47
+ return
48
+
49
+ filename.parent.mkdir(parents=True, exist_ok=True)
50
+ try:
51
+ url = provider.episode_link(anime, ep)
52
+ except Exception as e:
53
+ progress.console.print(f"[red]Errore link Ep. {ep.num}: {e}")
54
+ return
55
+
56
+ try:
57
+ async with AsyncClient() as client:
58
+ async with client.stream("GET", url, headers=provider.Client.headers) as response:
59
+ total = int(response.headers.get('content-length', 0))
60
+ progress.update(task_id, total=total)
61
+
62
+ downloaded = 0
63
+ temp_filename = filename.with_name(f"{filename.name}.temp")
64
+ with open(temp_filename, "wb") as f:
65
+ async for chunk in response.aiter_bytes(1024):
66
+ if chunk:
67
+ n = f.write(chunk)
68
+ downloaded += n
69
+ progress.update(task_id, advance=n)
70
+
71
+ temp_filename.rename(filename)
72
+ progress.update(task_id, description=f"[success]Ep. {ep.num} (Completato)[/]")
73
+ except Exception as e:
74
+ progress.console.print(f"[error]Errore download Ep. {ep.num}: {e}[/]")
75
+
76
+ async def _download_all():
77
+ concurrent_downloads = ut.config_data["general"].get("parallel-downloads", 3)
78
+ semaphore = asyncio.Semaphore(concurrent_downloads)
79
+ ordered_eps = sorted(episodes, key=lambda e: e.numeric())
80
+
81
+ with Progress(
82
+ TextColumn("[info]{task.description}[/]", style="info"),
83
+ BarColumn(bar_width=None),
84
+ "•",
85
+ DownloadColumn(),
86
+ "•",
87
+ TransferSpeedColumn(),
88
+ console=ut.console,
89
+ ) as progress:
90
+ tasks = []
91
+ for ep in ordered_eps:
92
+ task_id = progress.add_task(str(ep), total=None, start=True)
93
+ tasks.append(asyncio.create_task(download_worker(ep, task_id, progress, semaphore)))
94
+
95
+ await asyncio.gather(*tasks)
96
+
97
+ asyncio.run(_download_all())
@@ -0,0 +1,3 @@
1
+ from .fzf import Fzf
2
+
3
+ __all__ = ["Fzf"]