mirror of
https://github.com/Benexl/FastAnime.git
synced 2026-02-04 19:11:55 -08:00
feat: return some original menu options and functionality
This commit is contained in:
@@ -29,12 +29,12 @@ def episodes(ctx: Context, state: State) -> State | InternalDirective:
|
||||
return InternalDirective.BACKX2
|
||||
|
||||
chosen_episode: str | None = None
|
||||
start_time: str | None = None
|
||||
|
||||
if config.stream.continue_from_watch_history:
|
||||
# TODO: implement watch history logic
|
||||
pass
|
||||
chosen_episode, start_time = ctx.watch_history.get_episode(media_item)
|
||||
|
||||
if not chosen_episode:
|
||||
if not chosen_episode or ctx.switch.show_episodes_menu:
|
||||
choices = [*sorted(available_episodes, key=float), "Back"]
|
||||
|
||||
preview_command = None
|
||||
@@ -68,16 +68,10 @@ def episodes(ctx: Context, state: State) -> State | InternalDirective:
|
||||
|
||||
chosen_episode = chosen_episode_str
|
||||
|
||||
# Track episode selection in watch history (if enabled in config)
|
||||
if (
|
||||
config.stream.continue_from_watch_history
|
||||
and config.stream.preferred_watch_history == "local"
|
||||
):
|
||||
# TODO: implement watch history logic
|
||||
pass
|
||||
|
||||
return State(
|
||||
menu_name=MenuName.SERVERS,
|
||||
media_api=state.media_api,
|
||||
provider=state.provider.model_copy(update={"episode": chosen_episode}),
|
||||
provider=state.provider.model_copy(
|
||||
update={"episode": chosen_episode, "start_time": start_time}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Callable, Dict
|
||||
from typing import Callable, Dict, Literal, Optional
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
@@ -9,7 +9,7 @@ from .....libs.media_api.params import (
|
||||
MediaRelationsParams,
|
||||
UpdateUserMediaListEntryParams,
|
||||
)
|
||||
from .....libs.media_api.types import UserMediaListStatus
|
||||
from .....libs.media_api.types import MediaItem, MediaStatus, UserMediaListStatus
|
||||
from .....libs.player.params import PlayerParams
|
||||
from ...session import Context, session
|
||||
from ...state import InternalDirective, MediaApiState, MenuName, State
|
||||
@@ -28,11 +28,15 @@ def media_actions(ctx: Context, state: State) -> State | InternalDirective:
|
||||
if not media_item:
|
||||
feedback.error("Media item is not in state")
|
||||
return InternalDirective.BACK
|
||||
progress = _get_progress_string(ctx, state.media_api.media_item)
|
||||
|
||||
# TODO: Add media list management
|
||||
# TODO: cross reference for none implemented features
|
||||
options: Dict[str, MenuAction] = {
|
||||
f"{'▶️ ' if icons else ''}Stream": _stream(ctx, state),
|
||||
f"{'▶️ ' if icons else ''}Stream {progress}": _stream(ctx, state),
|
||||
f"{'📽️ ' if icons else ''}Episodes": _stream(
|
||||
ctx, state, force_episodes_menu=True
|
||||
),
|
||||
f"{'📼 ' if icons else ''}Watch Trailer": _watch_trailer(ctx, state),
|
||||
f"{'🔗 ' if icons else ''}Recommendations": _view_recommendations(ctx, state),
|
||||
f"{'🔄 ' if icons else ''}Related Anime": _view_relations(ctx, state),
|
||||
@@ -41,6 +45,19 @@ def media_actions(ctx: Context, state: State) -> State | InternalDirective:
|
||||
f"{'➕ ' if icons else ''}Add/Update List": _manage_user_media_list(ctx, state),
|
||||
f"{'⭐ ' if icons else ''}Score Anime": _score_anime(ctx, state),
|
||||
f"{'ℹ️ ' if icons else ''}View Info": _view_info(ctx, state),
|
||||
f"{'📀 ' if icons else ''}Change Provider": _change_provider(ctx, state),
|
||||
f"{'🔘 ' if icons else ''}Toggle Auto Select Anime (Current: {ctx.config.general.auto_select_anime_result})": _toggle_config_state(
|
||||
ctx, state, "AUTO_ANIME"
|
||||
),
|
||||
f"{'🔘 ' if icons else ''}Toggle Auto Next Episode (Current: {ctx.config.stream.auto_next})": _toggle_config_state(
|
||||
ctx, state, "AUTO_EPISODE"
|
||||
),
|
||||
f"{'🔘 ' if icons else ''}Toggle Continue From History (Current: {ctx.config.stream.continue_from_watch_history})": _toggle_config_state(
|
||||
ctx, state, "CONTINUE_FROM_HISTORY"
|
||||
),
|
||||
f"{'🔘 ' if icons else ''}Toggle Translation Type (Current: {ctx.config.stream.translation_type.upper()})": _toggle_config_state(
|
||||
ctx, state, "TRANSLATION_TYPE"
|
||||
),
|
||||
f"{'🔙 ' if icons else ''}Back to Results": lambda: InternalDirective.BACK,
|
||||
}
|
||||
|
||||
@@ -55,8 +72,39 @@ def media_actions(ctx: Context, state: State) -> State | InternalDirective:
|
||||
return InternalDirective.BACK
|
||||
|
||||
|
||||
def _stream(ctx: Context, state: State) -> MenuAction:
|
||||
def _get_progress_string(ctx: Context, media_item: Optional[MediaItem]) -> str:
|
||||
if not media_item:
|
||||
return ""
|
||||
config = ctx.config
|
||||
|
||||
progress = "0"
|
||||
|
||||
if media_item.user_status:
|
||||
progress = str(media_item.user_status.progress or 0)
|
||||
|
||||
episodes_total = str(media_item.episodes or "??")
|
||||
display_title = f"({progress} of {episodes_total})"
|
||||
|
||||
# Add a visual indicator for new episodes if applicable
|
||||
if (
|
||||
media_item.status == MediaStatus.RELEASING
|
||||
and media_item.next_airing
|
||||
and media_item.user_status
|
||||
and media_item.user_status.status == UserMediaListStatus.WATCHING
|
||||
):
|
||||
last_aired = media_item.next_airing.episode - 1
|
||||
unwatched = last_aired - (media_item.user_status.progress or 0)
|
||||
if unwatched > 0:
|
||||
icon = "🔹" if config.general.icons else "!"
|
||||
display_title += f" {icon}{unwatched} new{icon}"
|
||||
|
||||
return display_title
|
||||
|
||||
|
||||
def _stream(ctx: Context, state: State, force_episodes_menu=False) -> MenuAction:
|
||||
def action():
|
||||
if force_episodes_menu:
|
||||
ctx.switch.force_episodes_menu()
|
||||
return State(menu_name=MenuName.PROVIDER_SEARCH, media_api=state.media_api)
|
||||
|
||||
return action
|
||||
@@ -120,6 +168,47 @@ def _manage_user_media_list(ctx: Context, state: State) -> MenuAction:
|
||||
return action
|
||||
|
||||
|
||||
def _change_provider(ctx: Context, state: State) -> MenuAction:
|
||||
def action():
|
||||
from .....libs.provider.anime.types import ProviderName
|
||||
|
||||
new_provider = ctx.selector.choose(
|
||||
"Select Provider", [provider.value for provider in ProviderName]
|
||||
)
|
||||
ctx.config.general.provider = ProviderName(new_provider)
|
||||
return InternalDirective.RELOAD
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def _toggle_config_state(
|
||||
ctx: Context,
|
||||
state: State,
|
||||
config_state: Literal[
|
||||
"AUTO_ANIME", "AUTO_EPISODE", "CONTINUE_FROM_HISTORY", "TRANSLATION_TYPE"
|
||||
],
|
||||
) -> MenuAction:
|
||||
def action():
|
||||
match config_state:
|
||||
case "AUTO_ANIME":
|
||||
ctx.config.general.auto_select_anime_result = (
|
||||
not ctx.config.general.auto_select_anime_result
|
||||
)
|
||||
case "AUTO_EPISODE":
|
||||
ctx.config.stream.auto_next = not ctx.config.stream.auto_next
|
||||
case "CONTINUE_FROM_HISTORY":
|
||||
ctx.config.stream.continue_from_watch_history = (
|
||||
not ctx.config.stream.continue_from_watch_history
|
||||
)
|
||||
case "TRANSLATION_TYPE":
|
||||
ctx.config.stream.translation_type = (
|
||||
"sub" if ctx.config.stream.translation_type == "dub" else "dub"
|
||||
)
|
||||
return InternalDirective.RELOAD
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def _score_anime(ctx: Context, state: State) -> MenuAction:
|
||||
def action():
|
||||
feedback = ctx.feedback
|
||||
@@ -160,12 +249,13 @@ def _view_info(ctx: Context, state: State) -> MenuAction:
|
||||
if not media_item:
|
||||
return InternalDirective.RELOAD
|
||||
|
||||
import re
|
||||
|
||||
from rich import box
|
||||
from rich.columns import Columns
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
import re
|
||||
|
||||
from ....utils import image
|
||||
|
||||
@@ -532,8 +622,8 @@ def _view_characters(ctx: Context, state: State) -> MenuAction:
|
||||
|
||||
# Display characters using rich
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
@@ -621,12 +711,13 @@ def _view_airing_schedule(ctx: Context, state: State) -> MenuAction:
|
||||
return InternalDirective.RELOAD
|
||||
|
||||
# Display schedule using rich
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from datetime import datetime
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
console.clear()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Callable, Dict, Union
|
||||
from typing import Callable, Dict, Literal, Union
|
||||
|
||||
from ...session import Context, session
|
||||
from ...state import InternalDirective, MenuName, State
|
||||
@@ -42,9 +42,7 @@ def player_controls(ctx: Context, state: State) -> Union[State, InternalDirectiv
|
||||
return State(
|
||||
menu_name=MenuName.SERVERS,
|
||||
media_api=state.media_api,
|
||||
provider=state.provider.model_copy(
|
||||
update={"episode_number": next_episode_num}
|
||||
),
|
||||
provider=state.provider.model_copy(update={"episode": next_episode_num}),
|
||||
)
|
||||
|
||||
# --- Menu Options ---
|
||||
@@ -53,15 +51,22 @@ def player_controls(ctx: Context, state: State) -> Union[State, InternalDirectiv
|
||||
|
||||
if current_index < len(available_episodes) - 1:
|
||||
options[f"{'⏭️ ' if icons else ''}Next Episode"] = _next_episode(ctx, state)
|
||||
if current_index:
|
||||
options[f"{'⏪ ' if icons else ''}Previous Episode"] = _previous_episode(
|
||||
ctx, state
|
||||
)
|
||||
|
||||
options.update(
|
||||
{
|
||||
f"{'🔄 ' if icons else ''}Replay Episode": _replay(ctx, state),
|
||||
f"{'💻 ' if icons else ''}Change Server": _change_server(ctx, state),
|
||||
f"{'🎞️ ' if icons else ''}Back to Episode List": lambda: State(
|
||||
menu_name=MenuName.EPISODES,
|
||||
media_api=state.media_api,
|
||||
provider=state.provider,
|
||||
f"{'🔂 ' if icons else ''}Replay": _replay(ctx, state),
|
||||
f"{'💽 ' if icons else ''}Change Server": _change_server(ctx, state),
|
||||
f"{'📀 ' if icons else ''}Change Quality": _change_quality(ctx, state),
|
||||
f"{'🎞️ ' if icons else ''}Episode List": lambda: _episodes_list(ctx, state),
|
||||
f"{'🔘 ' if icons else ''}Toggle Auto Next Episode (Current: {ctx.config.stream.auto_next})": _toggle_config_state(
|
||||
ctx, state, "AUTO_EPISODE"
|
||||
),
|
||||
f"{'🔘 ' if icons else ''}Toggle Translation Type (Current: {ctx.config.stream.translation_type.upper()})": _toggle_config_state(
|
||||
ctx, state, "TRANSLATION_TYPE"
|
||||
),
|
||||
f"{'🏠 ' if icons else ''}Main Menu": lambda: State(
|
||||
menu_name=MenuName.MAIN
|
||||
@@ -81,7 +86,6 @@ def player_controls(ctx: Context, state: State) -> Union[State, InternalDirectiv
|
||||
def _next_episode(ctx: Context, state: State) -> MenuAction:
|
||||
def action():
|
||||
feedback = ctx.feedback
|
||||
feedback.clear_console()
|
||||
|
||||
config = ctx.config
|
||||
|
||||
@@ -113,7 +117,41 @@ def _next_episode(ctx: Context, state: State) -> MenuAction:
|
||||
menu_name=MenuName.SERVERS,
|
||||
media_api=state.media_api,
|
||||
provider=state.provider.model_copy(
|
||||
update={"episode_number": next_episode_num}
|
||||
update={"episode": next_episode_num}
|
||||
),
|
||||
)
|
||||
feedback.warning("This is the last available episode.")
|
||||
return InternalDirective.RELOAD
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def _previous_episode(ctx: Context, state: State) -> MenuAction:
|
||||
def action():
|
||||
feedback = ctx.feedback
|
||||
|
||||
config = ctx.config
|
||||
|
||||
provider_anime = state.provider.anime
|
||||
current_episode_num = state.provider.episode
|
||||
|
||||
if not provider_anime or not current_episode_num:
|
||||
feedback.error("Player state is incomplete. Returning.")
|
||||
return InternalDirective.BACK
|
||||
|
||||
available_episodes = getattr(
|
||||
provider_anime.episodes, config.stream.translation_type, []
|
||||
)
|
||||
current_index = available_episodes.index(current_episode_num)
|
||||
|
||||
if current_index:
|
||||
prev_episode_num = available_episodes[current_index - 1]
|
||||
|
||||
return State(
|
||||
menu_name=MenuName.SERVERS,
|
||||
media_api=state.media_api,
|
||||
provider=state.provider.model_copy(
|
||||
update={"episode": prev_episode_num}
|
||||
),
|
||||
)
|
||||
feedback.warning("This is the last available episode.")
|
||||
@@ -129,10 +167,39 @@ def _replay(ctx: Context, state: State) -> MenuAction:
|
||||
return action
|
||||
|
||||
|
||||
def _toggle_config_state(
|
||||
ctx: Context,
|
||||
state: State,
|
||||
config_state: Literal[
|
||||
"AUTO_ANIME", "AUTO_EPISODE", "CONTINUE_FROM_HISTORY", "TRANSLATION_TYPE"
|
||||
],
|
||||
) -> MenuAction:
|
||||
def action():
|
||||
match config_state:
|
||||
case "AUTO_ANIME":
|
||||
ctx.config.general.auto_select_anime_result = (
|
||||
not ctx.config.general.auto_select_anime_result
|
||||
)
|
||||
case "AUTO_EPISODE":
|
||||
ctx.config.stream.auto_next = not ctx.config.stream.auto_next
|
||||
case "CONTINUE_FROM_HISTORY":
|
||||
ctx.config.stream.continue_from_watch_history = (
|
||||
not ctx.config.stream.continue_from_watch_history
|
||||
)
|
||||
case "TRANSLATION_TYPE":
|
||||
ctx.config.stream.translation_type = (
|
||||
"sub" if ctx.config.stream.translation_type == "dub" else "dub"
|
||||
)
|
||||
return InternalDirective.RELOAD
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def _change_server(ctx: Context, state: State) -> MenuAction:
|
||||
def action():
|
||||
from .....libs.provider.anime.types import ProviderServer
|
||||
|
||||
feedback = ctx.feedback
|
||||
feedback.clear_console()
|
||||
|
||||
selector = ctx.selector
|
||||
|
||||
@@ -156,13 +223,37 @@ def _change_server(ctx: Context, state: State) -> MenuAction:
|
||||
"Select a different server:", list(server_map.keys())
|
||||
)
|
||||
if new_server_name:
|
||||
return State(
|
||||
menu_name=MenuName.SERVERS,
|
||||
media_api=state.media_api,
|
||||
provider=state.provider.model_copy(
|
||||
update={"selected_server": server_map[new_server_name]}
|
||||
),
|
||||
)
|
||||
ctx.config.stream.server = ProviderServer(new_server_name)
|
||||
return InternalDirective.RELOAD
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def _episodes_list(ctx: Context, state: State) -> MenuAction:
|
||||
def action():
|
||||
ctx.switch.force_episodes_menu()
|
||||
return InternalDirective.BACK
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def _change_quality(ctx: Context, state: State) -> MenuAction:
|
||||
def action():
|
||||
feedback = ctx.feedback
|
||||
|
||||
selector = ctx.selector
|
||||
|
||||
server_map = state.provider.servers
|
||||
|
||||
if not server_map:
|
||||
feedback.error("Player state is incomplete. Returning.")
|
||||
return InternalDirective.BACK
|
||||
|
||||
new_quality = selector.choose(
|
||||
"Select a different server:", list(["360", "480", "720", "1080"])
|
||||
)
|
||||
if new_quality:
|
||||
ctx.config.stream.quality = new_quality # type:ignore
|
||||
return InternalDirective.RELOAD
|
||||
|
||||
return action
|
||||
|
||||
@@ -31,7 +31,7 @@ def results(ctx: Context, state: State) -> State | InternalDirective:
|
||||
if page_info.has_next_page:
|
||||
choices.update(
|
||||
{
|
||||
f"{'➡️ ' if ctx.config.general.icons else ''}Next Page (Page {page_info.current_page + 1})": lambda: _handle_pagination(
|
||||
"Next Page (Page {page_info.current_page + 1})": lambda: _handle_pagination(
|
||||
ctx, state, 1
|
||||
)
|
||||
}
|
||||
@@ -39,7 +39,7 @@ def results(ctx: Context, state: State) -> State | InternalDirective:
|
||||
if page_info.current_page > 1:
|
||||
choices.update(
|
||||
{
|
||||
f"{'⬅️ ' if ctx.config.general.icons else ''}Previous Page (Page {page_info.current_page - 1})": lambda: _handle_pagination(
|
||||
"Previous Page (Page {page_info.current_page - 1})": lambda: _handle_pagination(
|
||||
ctx, state, -1
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ def servers(ctx: Context, state: State) -> State | InternalDirective:
|
||||
title=final_title,
|
||||
subtitles=[sub.url for sub in selected_server.subtitles],
|
||||
headers=selected_server.headers,
|
||||
start_time=state.provider.start_time,
|
||||
)
|
||||
)
|
||||
if media_item and episode_number:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import importlib.util
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional, Union
|
||||
|
||||
import click
|
||||
@@ -27,9 +27,49 @@ logger = logging.getLogger(__name__)
|
||||
MENUS_DIR = APP_DIR / "cli" / "interactive" / "menu"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Switch:
|
||||
"Forces menus to show selector and not just pass through,once viewed it auto sets back to false"
|
||||
|
||||
_provider_results: bool = False
|
||||
_episodes: bool = False
|
||||
_servers: bool = False
|
||||
|
||||
@property
|
||||
def show_provider_results_menu(self):
|
||||
if self._provider_results:
|
||||
self._provider_results = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def force_provider_results_menu(self):
|
||||
self._provider_results = True
|
||||
|
||||
@property
|
||||
def show_episodes_menu(self):
|
||||
if self._episodes:
|
||||
self._episodes = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def force_episodes_menu(self):
|
||||
self._episodes = True
|
||||
|
||||
@property
|
||||
def servers(self):
|
||||
if self._servers:
|
||||
self._servers = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def force_servers_menu(self):
|
||||
self._servers = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
config: "AppConfig"
|
||||
switch: Switch = field(default_factory=Switch)
|
||||
_provider: Optional["BaseAnimeProvider"] = None
|
||||
_selector: Optional["BaseSelector"] = None
|
||||
_player: Optional["BasePlayer"] = None
|
||||
|
||||
@@ -65,6 +65,7 @@ class ProviderState(StateModel):
|
||||
episode: Optional[str] = None
|
||||
servers: Optional[Dict[str, Server]] = None
|
||||
server_name: Optional[str] = None
|
||||
start_time: Optional[str] = None
|
||||
|
||||
@property
|
||||
def server(self) -> Optional[Server]:
|
||||
|
||||
@@ -44,6 +44,50 @@ class WatchHistoryService:
|
||||
)
|
||||
)
|
||||
|
||||
def get_episode(self, media_item: MediaItem):
|
||||
index_entry = self.media_registry.get_media_index_entry(media_item.id)
|
||||
current_remote_episode = None
|
||||
current_local_episode = None
|
||||
start_time = None
|
||||
episode = None
|
||||
|
||||
if media_item.user_status:
|
||||
# TODO: change mediaa item progress to a string
|
||||
current_remote_episode = str(media_item.user_status.progress)
|
||||
if index_entry:
|
||||
current_local_episode = index_entry.progress
|
||||
start_time = index_entry.last_watch_position
|
||||
total_duration = index_entry.total_duration
|
||||
if start_time and total_duration and current_local_episode:
|
||||
from ....core.utils.converter import calculate_completion_percentage
|
||||
|
||||
if (
|
||||
calculate_completion_percentage(start_time, total_duration)
|
||||
>= self.config.stream.episode_complete_at
|
||||
):
|
||||
start_time = None
|
||||
try:
|
||||
current_local_episode = str(int(current_local_episode) + 1)
|
||||
except:
|
||||
# incase its a float
|
||||
pass
|
||||
else:
|
||||
current_local_episode = current_remote_episode
|
||||
if not media_item.user_status:
|
||||
current_remote_episode = current_local_episode
|
||||
if current_local_episode != current_remote_episode:
|
||||
if self.config.general.preferred_tracker == "local":
|
||||
episode = current_local_episode
|
||||
else:
|
||||
episode = current_remote_episode
|
||||
else:
|
||||
episode = current_local_episode
|
||||
|
||||
# TODO: check if start time is mostly complete and increment the episode
|
||||
if episode == "0":
|
||||
episode = "1"
|
||||
return episode, start_time
|
||||
|
||||
def update(
|
||||
self,
|
||||
media_item: MediaItem,
|
||||
|
||||
@@ -3,6 +3,7 @@ from ..constants import APP_DATA_DIR, DEFAULTS_DIR, USER_VIDEOS_DIR
|
||||
# GeneralConfig
|
||||
GENERAL_PYGMENT_STYLE = "github-dark"
|
||||
GENERAL_API_CLIENT = "anilist"
|
||||
GENERAL_PREFERRED_TRACKER = "local"
|
||||
GENERAL_PROVIDER = "allanime"
|
||||
GENERAL_SELECTOR = "default"
|
||||
GENERAL_AUTO_SELECT_ANIME_RESULT = True
|
||||
|
||||
@@ -3,6 +3,9 @@ from .defaults import SESSIONS_DIR
|
||||
|
||||
GENERAL_PYGMENT_STYLE = "The pygment style to use"
|
||||
GENERAL_API_CLIENT = "The media database API to use (e.g., 'anilist', 'jikan')."
|
||||
GENERAL_PREFERRED_TRACKER = (
|
||||
"The preferred watch history tracker (local,remote) in cases of conflicts"
|
||||
)
|
||||
GENERAL_PROVIDER = "The default anime provider to use for scraping."
|
||||
GENERAL_SELECTOR = "The interactive selector tool to use for menus."
|
||||
GENERAL_AUTO_SELECT_ANIME_RESULT = (
|
||||
|
||||
@@ -14,6 +14,10 @@ from . import descriptions as desc
|
||||
class GeneralConfig(BaseModel):
|
||||
"""Configuration for general application behavior and integrations."""
|
||||
|
||||
preferred_tracker: Literal["local", "remote"] = Field(
|
||||
default=defaults.GENERAL_PREFERRED_TRACKER,
|
||||
description=desc.GENERAL_PREFERRED_TRACKER,
|
||||
)
|
||||
pygment_style: str = Field(
|
||||
default=defaults.GENERAL_PYGMENT_STYLE, description=desc.GENERAL_PYGMENT_STYLE
|
||||
)
|
||||
|
||||
@@ -8,3 +8,32 @@ def time_to_seconds(time_str: str) -> int:
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def calculate_completion_percentage(last_watch_time: str, total_duration: str) -> float:
|
||||
"""
|
||||
Calculates the percentage completion based on last watch time and total duration.
|
||||
|
||||
Args:
|
||||
last_watch_time: A string representing the last watched time in 'HH:MM:SS' format.
|
||||
total_duration: A string representing the total duration in 'HH:MM:SS' format.
|
||||
|
||||
Returns:
|
||||
A float representing the percentage completion (0.0 to 100.0).
|
||||
Returns 0.0 if total_duration is '00:00:00'.
|
||||
Caps the percentage at 100.0 if last_watch_time exceeds total_duration.
|
||||
|
||||
Raises:
|
||||
ValueError: If the input time strings are not in the expected format.
|
||||
"""
|
||||
last_watch_seconds = time_to_seconds(last_watch_time)
|
||||
total_duration_seconds = time_to_seconds(total_duration)
|
||||
|
||||
if total_duration_seconds == 0:
|
||||
return 0.0 # Avoid division by zero, return 0% for zero duration
|
||||
|
||||
# Calculate raw percentage
|
||||
percentage = (last_watch_seconds / total_duration_seconds) * 100.0
|
||||
|
||||
# Ensure percentage does not exceed 100%
|
||||
return min(percentage, 100.0)
|
||||
|
||||
@@ -11,5 +11,6 @@ class PlayerResult:
|
||||
total_time: The total duration of the media (e.g., "00:23:45").
|
||||
"""
|
||||
|
||||
episode: str | None = None
|
||||
stop_time: str | None = None
|
||||
total_time: str | None = None
|
||||
|
||||
Reference in New Issue
Block a user