diff --git a/fastanime/core/patterns.py b/fastanime/core/patterns.py new file mode 100644 index 0000000..d1fb147 --- /dev/null +++ b/fastanime/core/patterns.py @@ -0,0 +1,9 @@ +import re + +YOUTUBE_REGEX = re.compile( + r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/.+", re.IGNORECASE +) +TORRENT_REGEX = re.compile( + r"^(?:(magnet:\?xt=urn:btih:(?:[a-zA-Z0-9]{32}|[a-zA-Z0-9]{40}).*)|(https?://.*\.torrent))$", + re.IGNORECASE, +) diff --git a/fastanime/core/utils/detect.py b/fastanime/core/utils/detect.py new file mode 100644 index 0000000..b035e03 --- /dev/null +++ b/fastanime/core/utils/detect.py @@ -0,0 +1,18 @@ +import os +import sys + + +def is_running_in_termux(): + # Check environment variables + if os.environ.get("TERMUX_VERSION") is not None: + return True + + # Check Python installation path + if sys.prefix.startswith("/data/data/com.termux/files/usr"): + return True + + # Check for Termux-specific binary + if os.path.exists("/data/data/com.termux/files/usr/bin/termux-info"): + return True + + return False diff --git a/fastanime/libs/players/base.py b/fastanime/libs/players/base.py index ba42625..7c2ae90 100644 --- a/fastanime/libs/players/base.py +++ b/fastanime/libs/players/base.py @@ -1,23 +1,7 @@ from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import TYPE_CHECKING, List, Tuple -if TYPE_CHECKING: - from ..providers.anime.types import Subtitle - - -@dataclass(frozen=True) -class PlayerResult: - """ - Represents the result of a completed playback session. - - Attributes: - stop_time: The timestamp where playback stopped (e.g., "00:15:30"). - total_time: The total duration of the media (e.g., "00:23:45"). - """ - - stop_time: str | None = None - total_time: str | None = None +from .params import PlayerParams +from .types import PlayerResult class BasePlayer(ABC): @@ -26,25 +10,8 @@ class BasePlayer(ABC): """ @abstractmethod - def play( - self, - url: str, - title: str, - subtitles: List["Subtitle"] | None = None, - headers: dict | None = None, - start_time: str = "0", - ) -> PlayerResult: + def play(self, params: PlayerParams) -> PlayerResult: """ Plays the given media URL. - - Args: - url: The stream URL to play. - title: The title to display in the player window. - subtitles: A list of subtitle objects. - headers: Any required HTTP headers for the stream. - start_time: The timestamp to start playback from (e.g., "00:10:30"). - - Returns: - A tuple containing (stop_time, total_time) as strings. """ pass diff --git a/fastanime/libs/players/mpv/__init__.py b/fastanime/libs/players/mpv/__init__.py index 12f8561..8b13789 100644 --- a/fastanime/libs/players/mpv/__init__.py +++ b/fastanime/libs/players/mpv/__init__.py @@ -1 +1 @@ -from .player import MpvPlayer + diff --git a/fastanime/libs/players/mpv/player.py b/fastanime/libs/players/mpv/player.py index 14b3647..d243284 100644 --- a/fastanime/libs/players/mpv/player.py +++ b/fastanime/libs/players/mpv/player.py @@ -4,7 +4,12 @@ import shutil import subprocess from ....core.config import MpvConfig -from ..base import BasePlayer, PlayerResult +from ....core.exceptions import FastAnimeError +from ....core.patterns import TORRENT_REGEX, YOUTUBE_REGEX +from ....core.utils import detect +from ..base import BasePlayer +from ..params import PlayerParams +from ..types import PlayerResult logger = logging.getLogger(__name__) @@ -16,42 +21,71 @@ class MpvPlayer(BasePlayer): self.config = config self.executable = shutil.which("mpv") - def play(self, url, title, subtitles=None, headers=None, start_time="0"): + def play(self, params): + if TORRENT_REGEX.match(params.url) and detect.is_running_in_termux(): + raise FastAnimeError("Unable to play torrents on termux") + elif detect.is_running_in_termux(): + return self._play_on_mobile(params) + else: + return self._play_on_desktop(params) + + def _play_on_mobile(self, params) -> PlayerResult: + if YOUTUBE_REGEX.match(params.url): + args = [ + "nohup", + "am", + "start", + "--user", + "0", + "-a", + "android.intent.action.VIEW", + "-d", + params.url, + "-n", + "com.google.android.youtube/.UrlActivity", + ] + else: + args = [ + "nohup", + "am", + "start", + "--user", + "0", + "-a", + "android.intent.action.VIEW", + "-d", + params.url, + "-n", + "is.xyz.mpv/.MPVActivity", + ] + + subprocess.run(args) + + return PlayerResult() + + def _play_on_desktop(self, params) -> PlayerResult: if not self.executable: - raise FileNotFoundError("MPV executable not found in PATH.") + raise FastAnimeError("MPV executable not found in PATH.") - mpv_args = [] - if headers: - header_str = ",".join([f"{k}:{v}" for k, v in headers.items()]) - mpv_args.append(f"--http-header-fields={header_str}") + if TORRENT_REGEX.search(params.url): + return self._stream_on_desktop_with_webtorrent_cli(params) + elif self.config.use_python_mpv: + return self._stream_on_desktop_with_python_mpv(params) + else: + return self._stream_on_desktop_with_subprocess(params) - if subtitles: - for sub in subtitles: - mpv_args.append(f"--sub-file={sub.url}") + def _stream_on_desktop_with_subprocess(self, params: PlayerParams) -> PlayerResult: + mpv_args = [self.executable, params.url] - if start_time != "0": - mpv_args.append(f"--start={start_time}") - - if title: - mpv_args.append(f"--title={title}") - - if self.config.args: - mpv_args.extend(self.config.args.split(",")) + mpv_args.extend(self._create_mpv_cli_options(params)) pre_args = self.config.pre_args.split(",") if self.config.pre_args else [] - if self.config.use_python_mpv: - self._stream_with_python_mpv() - else: - self._stream_with_subprocess(self.executable, url, [], pre_args) - return PlayerResult() - - def _stream_with_subprocess(self, mpv_executable, url, mpv_args, pre_args): - last_time = "0" - total_time = "0" + stop_time = None + total_time = None proc = subprocess.run( - pre_args + [mpv_executable, url, *mpv_args], + pre_args + mpv_args, capture_output=True, text=True, encoding="utf-8", @@ -61,10 +95,57 @@ class MpvPlayer(BasePlayer): for line in reversed(proc.stdout.split("\n")): match = MPV_AV_TIME_PATTERN.search(line.strip()) if match: - last_time = match.group(1) + stop_time = match.group(1) total_time = match.group(2) break - return last_time, total_time + return PlayerResult(total_time=total_time, stop_time=stop_time) - def _stream_with_python_mpv(self): - return "0", "0" + def _stream_on_desktop_with_python_mpv(self, params: PlayerParams) -> PlayerResult: + return PlayerResult() + + def _stream_on_desktop_with_webtorrent_cli( + self, params: PlayerParams + ) -> PlayerResult: + WEBTORRENT_CLI = shutil.which("webtorrent") + if not WEBTORRENT_CLI: + raise FastAnimeError( + "Please Install webtorrent cli inorder to stream torrents" + ) + + args = [WEBTORRENT_CLI, params.url, "--mpv"] + if mpv_args := self._create_mpv_cli_options(params): + args.append("--player-args") + args.extend(mpv_args) + + subprocess.run(args) + return PlayerResult() + + def _create_mpv_cli_options(self, params: PlayerParams) -> list[str]: + mpv_args = [] + if params.headers: + header_str = ",".join([f"{k}:{v}" for k, v in params.headers.items()]) + mpv_args.append(f"--http-header-fields={header_str}") + + if params.subtitles: + for sub in params.subtitles: + mpv_args.append(f"--sub-file={sub.url}") + + if params.start_time: + mpv_args.append(f"--start={params.start_time}") + + if params.title: + mpv_args.append(f"--title={params.title}") + + if self.config.args: + mpv_args.extend(self.config.args.split(",")) + return mpv_args + + +if __name__ == "__main__": + from ....core.constants import APP_ASCII_ART + + print(APP_ASCII_ART) + url = input("Enter the url you would like to stream: ") + mpv = MpvPlayer(MpvConfig()) + player_result = mpv.play(PlayerParams(url=url, title="")) + print(player_result) diff --git a/fastanime/libs/players/params.py b/fastanime/libs/players/params.py new file mode 100644 index 0000000..9b69af1 --- /dev/null +++ b/fastanime/libs/players/params.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass + + +@dataclass +class Subtitle: + url: str + language: str | None = None + + +@dataclass(frozen=True) +class PlayerParams: + url: str + title: str + subtitles: list[Subtitle] | None = None + headers: dict[str, str] | None = None + start_time: str | None = None diff --git a/fastanime/libs/players/player.py b/fastanime/libs/players/player.py index d5b4d2d..8d886fb 100644 --- a/fastanime/libs/players/player.py +++ b/fastanime/libs/players/player.py @@ -1,7 +1,3 @@ -from typing import TYPE_CHECKING - -# from .vlc.player import VlcPlayer # When you create it -# from .syncplay.player import SyncplayPlayer # When you create it from ...core.config import AppConfig from .base import BasePlayer @@ -31,7 +27,7 @@ class PlayerFactory: ) if player_name == "mpv": - from .mpv import MpvPlayer + from .mpv.player import MpvPlayer return MpvPlayer(config.mpv) raise NotImplementedError( diff --git a/fastanime/libs/players/types.py b/fastanime/libs/players/types.py new file mode 100644 index 0000000..02b04cb --- /dev/null +++ b/fastanime/libs/players/types.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class PlayerResult: + """ + Represents the result of a completed playback session. + + Attributes: + stop_time: The timestamp where playback stopped (e.g., "00:15:30"). + total_time: The total duration of the media (e.g., "00:23:45"). + """ + + stop_time: str | None = None + total_time: str | None = None diff --git a/fastanime/libs/providers/anime/types.py b/fastanime/libs/providers/anime/types.py index 37f718f..14c2b5e 100644 --- a/fastanime/libs/providers/anime/types.py +++ b/fastanime/libs/providers/anime/types.py @@ -74,6 +74,6 @@ class Server(BaseAnimeProviderModel): name: str links: list[EpisodeStream] episode_title: str | None = None - headers: dict | None = None - subtitles: list[Subtitle] | None = None - audio: list["str"] | None = None + headers: dict[str, str] = dict() + subtitles: list[Subtitle] = [] + audio: list[str] = []