From 571bf7de8327be3dbaec4878c08a1804649c543e Mon Sep 17 00:00:00 2001 From: Benex254 Date: Mon, 5 Aug 2024 09:47:04 +0300 Subject: [PATCH] feat(player): implement continue from timestamp --- fastanime/cli/config.py | 14 +++- .../cli/interfaces/anilist_interfaces.py | 69 +++++++++++++++++-- fastanime/cli/utils/mpv.py | 54 +++++++++++++-- 3 files changed, 123 insertions(+), 14 deletions(-) diff --git a/fastanime/cli/config.py b/fastanime/cli/config.py index 5dafa24..930a5e1 100644 --- a/fastanime/cli/config.py +++ b/fastanime/cli/config.py @@ -70,8 +70,18 @@ class Config(object): self.user = user user_data_helper.update_user_info(user) - def update_watch_history(self, anime_id: int, episode: str | None): - self.watch_history.update({str(anime_id): episode}) + def update_watch_history( + self, anime_id: int, episode: str | None, start_time="0", total_time="0" + ): + self.watch_history.update( + { + str(anime_id): { + "episode": episode, + "start_time": start_time, + "total_time": total_time, + } + } + ) user_data_helper.update_watch_history(self.watch_history) def update_anime_list(self, anime_id: int, remove=False): diff --git a/fastanime/cli/interfaces/anilist_interfaces.py b/fastanime/cli/interfaces/anilist_interfaces.py index 36381a0..a1aa61f 100644 --- a/fastanime/cli/interfaces/anilist_interfaces.py +++ b/fastanime/cli/interfaces/anilist_interfaces.py @@ -2,6 +2,7 @@ from __future__ import annotations import os import random +from datetime import datetime from rich import print from rich.progress import Progress @@ -20,6 +21,19 @@ from ..utils.tools import QueryDict, exit_app from ..utils.utils import clear, fuzzy_inquirer +def calculate_time_delta(start_time, end_time): + time_format = "%H:%M:%S" + + # Convert string times to datetime objects + start = datetime.strptime(start_time, time_format) + end = datetime.strptime(end_time, time_format) + + # Calculate the difference + delta = end - start + + return delta + + def player_controls(config: Config, anilist_config: QueryDict): # user config config.translation_type.lower() @@ -46,8 +60,25 @@ def player_controls(config: Config, anilist_config: QueryDict): current_episode, ) - mpv(current_link, selected_server["episode_title"]) + start_time = config.watch_history[str(anime_id)]["start_time"] + print("[green]Continuing from:[/] ", start_time) + stop_time, total_time = mpv( + current_link, selected_server["episode_title"], start_time=start_time + ) + if stop_time == "0": + episode = str(int(current_episode) + 1) + else: + error = 5 * 60 + delta = calculate_time_delta(stop_time, total_time) + if delta.total_seconds() > error: + episode = current_episode + else: + episode = str(int(current_episode) + 1) + stop_time = "0" + total_time = "0" + clear() + config.update_watch_history(anime_id, episode, stop_time, total_time) player_controls(config, anilist_config) def _next_episode(): @@ -220,14 +251,34 @@ def fetch_streams(config: Config, anilist_config: QueryDict): { "mediaId": anime_id, "status": "CURRENT", - "progress": episode_number if episode_number else 1, + "progress": episode_number if episode_number else 0, } ) - mpv(stream_link, selected_server["episode_title"]) + start_time = config.watch_history.get(str(anime_id), {}).get("start_time", "0") + if start_time != "0": + print("[green]Continuing from:[/] ", start_time) + stop_time, total_time = mpv( + stream_link, selected_server["episode_title"], start_time=start_time + ) + print("Finished at: ", stop_time) # update_watch_history - config.update_watch_history(anime_id, str(int(episode_number) + 1)) + if stop_time == "0": + episode = str(int(episode_number) + 1) + else: + error = 5 * 60 + delta = calculate_time_delta(stop_time, total_time) + if delta.total_seconds() > error: + episode = episode_number + else: + episode = str(int(episode_number) + 1) + stop_time = "0" + total_time = "0" + + config.update_watch_history( + anime_id, episode, start_time=stop_time, total_time=total_time + ) # switch to controls clear() @@ -249,8 +300,11 @@ def fetch_episode(config: Config, anilist_config: QueryDict): # prompt for episode number episodes = anime["availableEpisodesDetail"][translation_type] - if continue_from_history and user_watch_history.get(str(anime_id)) in episodes: - episode_number = user_watch_history[str(anime_id)] + if ( + continue_from_history + and user_watch_history.get(str(anime_id), {}).get("episode") in episodes + ): + episode_number = user_watch_history[str(anime_id)]["episode"] print(f"[bold cyan]Continuing from Episode:[/] [bold]{episode_number}[/]") else: choices = [*episodes, "Back"] @@ -266,7 +320,8 @@ def fetch_episode(config: Config, anilist_config: QueryDict): if episode_number == "Back": anilist_options(config, anilist_config) return - config.update_watch_history(anime_id, episode_number) + start_time = user_watch_history.get(str(anime_id), {}).get("start_time", "0") + config.update_watch_history(anime_id, episode_number, start_time=start_time) # update internal config anilist_config.episodes = episodes diff --git a/fastanime/cli/utils/mpv.py b/fastanime/cli/utils/mpv.py index 3e73839..810ac01 100644 --- a/fastanime/cli/utils/mpv.py +++ b/fastanime/cli/utils/mpv.py @@ -3,7 +3,6 @@ import shutil import subprocess from typing import Optional - # legacy # def mpv(link, title: None | str = "anime", *custom_args): # MPV = shutil.which("mpv") @@ -25,8 +24,50 @@ from typing import Optional # else: # subprocess.run([MPV, *custom_args, f"--title={title}", link]) # -# -def mpv(link: str, title: Optional[str] = "anime", *custom_args): + + +def stream_video(url, mpv_args): + process = subprocess.Popen( + ["mpv", url, *mpv_args], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + last_time = None + av_time_pattern = re.compile(r"AV: ([0-9:]*) / ([0-9:]*) \(([0-9]*)%\)") + last_time = "0" + total_time = "0" + + try: + while True: + output = process.stderr.readline() + + if output: + # Match the timestamp in the output + match = av_time_pattern.search(output.strip()) + if match: + current_time = match.group(1) + total_time = match.group(2) + match.group(3) + last_time = current_time + # print(f"Current stream time: {current_time}, Total time: {total_time}, Progress: {percentage}%") + + # Check if the process has terminated + retcode = process.poll() + if retcode is not None: + print("Finshed at: ", last_time) + break + + except Exception as e: + print(f"An error occurred: {e}") + finally: + process.terminate() + + return last_time, total_time + + +def mpv(link: str, title: Optional[str] = "anime", start_time: str = "0", *custom_args): # Determine if mpv is available MPV = shutil.which("mpv") @@ -54,6 +95,7 @@ def mpv(link: str, title: Optional[str] = "anime", *custom_args): "-n", "com.google.android.youtube/.UrlActivity", ] + return "0" else: # Android specific commands to launch mpv with a regular URL args = [ @@ -71,10 +113,12 @@ def mpv(link: str, title: Optional[str] = "anime", *custom_args): ] subprocess.run(args) + return "0" else: # General mpv command with custom arguments - mpv_args = [MPV, *custom_args, f"--title={title}", link] - subprocess.run(mpv_args) + mpv_args = [*custom_args, f"--title={title}", f"--start={start_time}"] + stop_time, total_time = stream_video(link, mpv_args) + return stop_time, total_time # Example usage