feat: player mpv

This commit is contained in:
Benexl
2025-07-12 22:55:13 +03:00
parent 18a9b07144
commit 723a7ab24f
9 changed files with 179 additions and 77 deletions

View 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,
)

View 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

View File

@@ -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

View File

@@ -1 +1 @@
from .player import MpvPlayer

View File

@@ -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)

View 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

View File

@@ -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(

View 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

View File

@@ -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] = []