mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-12 15:50:01 -08:00
feat: player mpv
This commit is contained in:
9
fastanime/core/patterns.py
Normal file
9
fastanime/core/patterns.py
Normal file
@@ -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,
|
||||
)
|
||||
18
fastanime/core/utils/detect.py
Normal file
18
fastanime/core/utils/detect.py
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
from .player import MpvPlayer
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
16
fastanime/libs/players/params.py
Normal file
16
fastanime/libs/players/params.py
Normal file
@@ -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
|
||||
@@ -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(
|
||||
|
||||
15
fastanime/libs/players/types.py
Normal file
15
fastanime/libs/players/types.py
Normal file
@@ -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
|
||||
@@ -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] = []
|
||||
|
||||
Reference in New Issue
Block a user