Skip to content
This repository was archived by the owner on May 9, 2026. It is now read-only.

Commit 449f6c1

Browse files
committed
feat(interactive-state): create accessors that ensure values exist
1 parent ab4734b commit 449f6c1

8 files changed

Lines changed: 115 additions & 63 deletions

File tree

viu_cli/cli/interactive/menu/media/episodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,6 @@ def episodes(ctx: Context, state: State) -> State | InternalDirective:
7272
menu_name=MenuName.SERVERS,
7373
media_api=state.media_api,
7474
provider=state.provider.model_copy(
75-
update={"episode": chosen_episode, "start_time": start_time}
75+
update={"episode_": chosen_episode, "start_time_": start_time}
7676
),
7777
)

viu_cli/cli/interactive/menu/media/play_downloads.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ def play_downloads(ctx: Context, state: State) -> State | InternalDirective:
1515
feedback = ctx.feedback
1616
media_item = state.media_api.media_item
1717
current_episode_num = state.provider.episode
18-
if not media_item:
19-
feedback.error("No media item selected.")
20-
return InternalDirective.BACK
2118

2219
record = ctx.media_registry.get_media_record(media_item.id)
2320
if not record or not record.media_episodes:
@@ -65,9 +62,7 @@ def play_downloads(ctx: Context, state: State) -> State | InternalDirective:
6562
return InternalDirective.BACK
6663

6764
chosen_episode = chosen_episode_str
68-
# Workers are automatically cleaned up when exiting the context
6965
else:
70-
# No preview mode
7166
chosen_episode_str = ctx.selector.choose(
7267
prompt="Select Episode", choices=choices, preview=None
7368
)
@@ -84,7 +79,7 @@ def play_downloads(ctx: Context, state: State) -> State | InternalDirective:
8479
menu_name=MenuName.DOWNLOADS_PLAYER_CONTROLS,
8580
media_api=state.media_api,
8681
provider=state.provider.model_copy(
87-
update={"episode": chosen_episode, "start_time": start_time}
82+
update={"episode_": chosen_episode, "start_time_": start_time}
8883
),
8984
)
9085

@@ -155,7 +150,7 @@ def downloads_player_controls(
155150
menu_name=MenuName.DOWNLOADS_PLAYER_CONTROLS,
156151
media_api=state.media_api,
157152
provider=state.provider.model_copy(
158-
update={"episode": next_episode_num, "start_time": None}
153+
update={"episode_": next_episode_num, "start_time_": None}
159154
),
160155
)
161156

@@ -230,7 +225,7 @@ def action():
230225
menu_name=MenuName.DOWNLOADS_PLAYER_CONTROLS,
231226
media_api=state.media_api,
232227
provider=state.provider.model_copy(
233-
update={"episode": next_episode_num, "start_time": None}
228+
update={"episode_": next_episode_num, "start_time_": None}
234229
),
235230
)
236231
feedback.warning("This is the last available episode.")
@@ -279,7 +274,7 @@ def action():
279274
menu_name=MenuName.DOWNLOADS_PLAYER_CONTROLS,
280275
media_api=state.media_api,
281276
provider=state.provider.model_copy(
282-
update={"episode": prev_episode_num, "start_time": None}
277+
update={"episode_": prev_episode_num, "start_time_": None}
283278
),
284279
)
285280
feedback.warning("This is the last available episode.")

viu_cli/cli/interactive/menu/media/player_controls.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def player_controls(ctx: Context, state: State) -> Union[State, InternalDirectiv
4242
return State(
4343
menu_name=MenuName.SERVERS,
4444
media_api=state.media_api,
45-
provider=state.provider.model_copy(update={"episode": next_episode_num}),
45+
provider=state.provider.model_copy(update={"episode_": next_episode_num}),
4646
)
4747

4848
# --- Menu Options ---
@@ -116,7 +116,7 @@ def action():
116116
menu_name=MenuName.SERVERS,
117117
media_api=state.media_api,
118118
provider=state.provider.model_copy(
119-
update={"episode": next_episode_num}
119+
update={"episode_": next_episode_num}
120120
),
121121
)
122122
feedback.warning("This is the last available episode.")
@@ -150,7 +150,7 @@ def action():
150150
menu_name=MenuName.SERVERS,
151151
media_api=state.media_api,
152152
provider=state.provider.model_copy(
153-
update={"episode": prev_episode_num}
153+
update={"episode_": prev_episode_num}
154154
),
155155
)
156156
feedback.warning("This is the last available episode.")

viu_cli/cli/interactive/menu/media/provider_search.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@
77
@session.menu
88
def provider_search(ctx: Context, state: State) -> State | InternalDirective:
99
from viu_cli.cli.utils.search import find_best_match_title
10+
1011
from .....core.utils.normalizer import normalize_title, update_user_normalizer_json
1112

1213
feedback = ctx.feedback
1314
media_item = state.media_api.media_item
14-
if not media_item:
15-
feedback.error("No AniList anime to search for", "Please select an anime first")
16-
return InternalDirective.BACK
1715

1816
provider = ctx.provider
1917
selector = ctx.selector

viu_cli/cli/interactive/menu/media/results.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@ def results(ctx: Context, state: State) -> State | InternalDirective:
1313
feedback.clear_console()
1414

1515
search_result = state.media_api.search_result
16-
page_info = state.media_api.page_info
17-
18-
if not search_result:
19-
feedback.info("No anime found for the given criteria")
20-
return InternalDirective.BACK
16+
page_info = state.media_api.page_info_
2117

2218
search_result_dict = {
2319
_format_title(ctx, media_item): media_item

viu_cli/cli/interactive/menu/media/servers.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ def servers(ctx: Context, state: State) -> State | InternalDirective:
1818
provider_anime = state.provider.anime
1919
media_item = state.media_api.media_item
2020

21-
if not media_item:
22-
return InternalDirective.BACK
2321
anime_title = media_item.title.romaji or media_item.title.english
2422
episode_number = state.provider.episode
2523

@@ -106,8 +104,8 @@ def servers(ctx: Context, state: State) -> State | InternalDirective:
106104
media_api=state.media_api,
107105
provider=state.provider.model_copy(
108106
update={
109-
"servers": server_map,
110-
"server_name": selected_server.name,
107+
"servers_": server_map,
108+
"server_name_": selected_server.name,
111109
}
112110
),
113111
)

viu_cli/cli/interactive/state.py

Lines changed: 102 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,15 @@
1+
from __future__ import annotations
2+
13
from enum import Enum
2-
from typing import Dict, Optional, Union
4+
from typing import Any, Dict, Mapping, Optional, Union
35

4-
from pydantic import BaseModel, ConfigDict, Field
6+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, computed_field
57

68
from ...libs.media_api.params import MediaSearchParams, UserMediaListSearchParams
79
from ...libs.media_api.types import MediaItem, PageInfo
810
from ...libs.provider.anime.types import Anime, SearchResults, Server
911

1012

11-
# TODO: is internal directive a good name
12-
class InternalDirective(Enum):
13-
MAIN = "MAIN"
14-
15-
BACK = "BACK"
16-
17-
BACKX2 = "BACKX2"
18-
19-
BACKX3 = "BACKX3"
20-
21-
BACKX4 = "BACKX4"
22-
23-
EXIT = "EXIT"
24-
25-
CONFIG_EDIT = "CONFIG_EDIT"
26-
27-
RELOAD = "RELOAD"
28-
29-
3013
class MenuName(Enum):
3114
MAIN = "MAIN"
3215
AUTH = "AUTH"
@@ -49,34 +32,116 @@ class MenuName(Enum):
4932
DOWNLOAD_EPISODES = "DOWNLOAD_EPISODES"
5033

5134

35+
class InternalDirective(Enum):
36+
MAIN = "MAIN"
37+
38+
BACK = "BACK"
39+
40+
BACKX2 = "BACKX2"
41+
42+
BACKX3 = "BACKX3"
43+
44+
BACKX4 = "BACKX4"
45+
46+
EXIT = "EXIT"
47+
48+
CONFIG_EDIT = "CONFIG_EDIT"
49+
50+
RELOAD = "RELOAD"
51+
52+
5253
class StateModel(BaseModel):
5354
model_config = ConfigDict(frozen=True)
5455

5556

5657
class MediaApiState(StateModel):
57-
search_result: Optional[Dict[int, MediaItem]] = None
58-
search_params: Optional[Union[MediaSearchParams, UserMediaListSearchParams]] = None
59-
page_info: Optional[PageInfo] = None
60-
media_id: Optional[int] = None
58+
search_result_: Optional[Dict[int, MediaItem]] = Field(
59+
default=None, alias="search_result"
60+
)
61+
search_params_: Optional[Union[MediaSearchParams, UserMediaListSearchParams]] = (
62+
Field(default=None, alias="search_params")
63+
)
64+
page_info_: Optional[PageInfo] = Field(default=None, alias="page_info")
65+
media_id_: Optional[int] = Field(default=None, alias="media_id")
66+
67+
@property
68+
def search_result(self) -> dict[int, MediaItem]:
69+
if not self.search_result_:
70+
raise RuntimeError("Malformed state, please report")
71+
return self.search_result_
72+
73+
@property
74+
def search_params(self) -> Union[MediaSearchParams, UserMediaListSearchParams]:
75+
if not self.search_params_:
76+
raise RuntimeError("Malformed state, please report")
77+
return self.search_params_
78+
79+
@property
80+
def page_info(self) -> PageInfo | None:
81+
# if not self._page_info:
82+
# raise RuntimeError("Malformed state, please report")
83+
return self.page_info_
6184

6285
@property
63-
def media_item(self) -> Optional[MediaItem]:
64-
if self.search_result and self.media_id:
65-
return self.search_result[self.media_id]
86+
def media_id(self) -> int:
87+
if not self.media_id_:
88+
raise RuntimeError("Malformed state, please report")
89+
return self.media_id_
90+
91+
@property
92+
def media_item(self) -> MediaItem:
93+
return self.search_result[self.media_id]
6694

6795

6896
class ProviderState(StateModel):
69-
search_results: Optional[SearchResults] = None
70-
anime: Optional[Anime] = None
71-
episode: Optional[str] = None
72-
servers: Optional[Dict[str, Server]] = None
73-
server_name: Optional[str] = None
74-
start_time: Optional[str] = None
97+
search_results_: Optional[SearchResults] = Field(
98+
default=None, alias="search_results"
99+
)
100+
anime_: Optional[Anime] = Field(default=None, alias="anime")
101+
episode_: Optional[str] = Field(default=None, alias="episode")
102+
servers_: Optional[Dict[str, Server]] = Field(default=None, alias="servers")
103+
server_name_: Optional[str] = Field(default=None, alias="server_name")
104+
start_time_: Optional[str] = Field(default=None, alias="start_time")
105+
106+
@property
107+
def search_results(self) -> SearchResults:
108+
if not self.search_results_:
109+
raise RuntimeError("Malformed state, please report")
110+
return self.search_results_
111+
112+
@property
113+
def anime(self) -> Anime:
114+
if not self.anime_:
115+
raise RuntimeError("Malformed state, please report")
116+
return self.anime_
117+
118+
@property
119+
def episode(self) -> str | None:
120+
# if not self._episode:
121+
# raise RuntimeError("Malformed state, please report")
122+
return self.episode_
123+
124+
@property
125+
def servers(self) -> Dict[str, Server]:
126+
if not self.servers_:
127+
raise RuntimeError("Malformed state, please report")
128+
return self.servers_
129+
130+
@property
131+
def server_name(self) -> str:
132+
if not self.server_name_:
133+
raise RuntimeError("Malformed state, please report")
134+
return self.server_name_
135+
136+
@property
137+
def start_time(self) -> str | None:
138+
# if not self._start_time:
139+
# raise RuntimeError("Malformed state, please report")
140+
return self.start_time_
75141

76142
@property
77-
def server(self) -> Optional[Server]:
78-
if self.servers and self.server_name:
79-
return self.servers[self.server_name]
143+
def server(self) -> Server:
144+
return self.servers[self.server_name]
80145

81146

82147
class State(StateModel):

viu_cli/cli/service/session/service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def get_most_recent_session_history(self) -> Optional[List[State]]:
6565
def _save_session(self, session: Session):
6666
path = self.dir / f"{session.name}.json"
6767
with AtomicWriter(path) as f:
68-
json.dump(session.model_dump(mode="json"), f)
68+
json.dump(session.model_dump(mode="json", by_alias=True), f)
6969

7070
def _load_session(self, session_name: str) -> Optional[Session]:
7171
path = self.dir / f"{session_name}.json"

0 commit comments

Comments
 (0)