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.
- {aw_cli-2.2 → aw_cli-2.3}/.github/workflows/python-publish.yml +32 -32
- aw_cli-2.3/.github/workflows/release.yml +19 -0
- {aw_cli-2.2 → aw_cli-2.3}/PKG-INFO +3 -1
- {aw_cli-2.2 → aw_cli-2.3}/pyproject.toml +2 -0
- {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/anilist.py +28 -26
- {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/anime.py +63 -26
- {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/arg_parser.py +19 -19
- aw_cli-2.3/src/aw_cli/download.py +97 -0
- aw_cli-2.3/src/aw_cli/fzf/__init__.py +3 -0
- aw_cli-2.3/src/aw_cli/fzf/fzf.py +192 -0
- {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/history.py +29 -25
- {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/providers/animeunity.py +3 -3
- {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/providers/animeworld.py +3 -3
- {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/providers/local.py +15 -6
- {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/providers/provider.py +32 -13
- aw_cli-2.3/src/aw_cli/run.py +589 -0
- {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/update.py +5 -4
- aw_cli-2.3/src/aw_cli/utilities.py +87 -0
- aw_cli-2.3/tests/test_fzf.py +97 -0
- {aw_cli-2.2 → aw_cli-2.3}/tests/test_history.py +3 -3
- aw_cli-2.3/tests/test_rich_integration.py +27 -0
- aw_cli-2.3/tests/test_run.py +34 -0
- {aw_cli-2.2 → aw_cli-2.3}/uv.lock +192 -0
- aw_cli-2.2/src/aw_cli/download.py +0 -111
- aw_cli-2.2/src/aw_cli/run.py +0 -560
- aw_cli-2.2/src/aw_cli/utilities.py +0 -107
- {aw_cli-2.2 → aw_cli-2.3}/.gitignore +0 -0
- {aw_cli-2.2 → aw_cli-2.3}/.pre-commit-config.yaml +0 -0
- {aw_cli-2.2 → aw_cli-2.3}/LICENSE +0 -0
- {aw_cli-2.2 → aw_cli-2.3}/README.md +0 -0
- {aw_cli-2.2 → aw_cli-2.3}/disclaimer.md +0 -0
- {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/__init__.py +0 -0
- {aw_cli-2.2 → aw_cli-2.3}/src/aw_cli/providers/__init__.py +0 -0
- {aw_cli-2.2 → aw_cli-2.3}/tests/__init__.py +0 -0
- {aw_cli-2.2 → aw_cli-2.3}/tests/empty +0 -0
- {aw_cli-2.2 → aw_cli-2.3}/tests/search_dragonball +0 -0
- {aw_cli-2.2 → aw_cli-2.3}/tests/search_onepiece +0 -0
- {aw_cli-2.2 → aw_cli-2.3}/tests/test_anime.py +0 -0
- {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.
|
|
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
|
|
@@ -3,25 +3,25 @@ import httpx
|
|
|
3
3
|
class TokenError(Exception):
|
|
4
4
|
pass
|
|
5
5
|
|
|
6
|
-
def
|
|
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
|
-
|
|
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 ($
|
|
19
|
-
SaveMediaListEntry (mediaId: $
|
|
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:$
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
if
|
|
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(
|
|
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
|
-
|
|
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 ($
|
|
87
|
-
MediaList(userId: $
|
|
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
|
-
"
|
|
94
|
-
"
|
|
95
|
+
"anime_id": anime_id,
|
|
96
|
+
"user_id": user_id
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
headers = {
|
|
98
100
|
'Authorization': f'Bearer {token}',
|
|
99
101
|
'Content-Type': 'application/json', 'Accept': 'application/json'
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
if
|
|
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(
|
|
107
|
-
|
|
108
|
+
return float(response.json()["data"]["MediaList"]["score"])
|
|
108
109
|
|
|
109
|
-
def
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
56
|
-
return self.
|
|
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.
|
|
62
|
-
return hash((self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
+
status (AnimeStatus): lo stato dell'anime.
|
|
125
|
+
info (dict): dizionario delle informazioni dell'anime.
|
|
119
126
|
"""
|
|
120
|
-
self.
|
|
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
|
|
132
|
+
def effective_episodes_count(self) -> int:
|
|
126
133
|
"""
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
|
170
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
14
|
+
latest = False
|
|
15
15
|
offline = False
|
|
16
|
-
|
|
16
|
+
hist = False
|
|
17
17
|
info = False
|
|
18
|
-
|
|
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='
|
|
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='
|
|
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
|
|
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='
|
|
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='
|
|
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
|
-
|
|
124
|
-
elif args.
|
|
125
|
-
|
|
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.
|
|
132
|
-
if args.
|
|
133
|
-
args.
|
|
134
|
-
|
|
135
|
-
if args.
|
|
136
|
-
|
|
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
|
-
|
|
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())
|