Last active
November 25, 2024 15:29
A Python script that allows you to search for anime, select episodes, and stream them using the MPV player.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from typing import Dict, Any, List | |
from anipy_api.provider.base import ProviderStream | |
from anipy_api.provider import list_providers, get_provider, LanguageTypeEnum | |
from anipy_api.anime import Anime | |
from pyfzf.pyfzf import FzfPrompt | |
from colorama import Fore, Style | |
from shutil import which | |
import os, subprocess | |
class Player: | |
@staticmethod | |
def mpv_check() -> bool: | |
return which('mpv') is not None | |
@staticmethod | |
def mpv_play_url(url: str, title: str, fullscreen: bool = True) -> None: | |
if Player.mpv_check(): | |
mpv_command: List[str] = ['mpv'] | |
if fullscreen: | |
mpv_command.append('--fs') | |
mpv_command.extend([f'--force-media-title={title}', url]) | |
stdout = open(os.devnull, 'w') | |
subprocess.Popen(mpv_command, stdout=stdout, stderr=subprocess.STDOUT) | |
else: | |
raise NoSuchPlayerFound('MPV player not found on your system.') | |
class AnimeExtractor: | |
def __init__(self, search_result: Dict[str, Any], provider) -> None: | |
self.provider = provider() | |
self.search_results: List[Any] = [r for r in search_result.values()] | |
self.anime_list: List[Anime] = [] | |
def get_anime_infos(self) -> List[Anime]: | |
for result in self.search_results: | |
self.anime_list.append(Anime.from_search_result(self.provider, result)) | |
return self.anime_list | |
def list_episodes(self, anime: Anime, lang=LanguageTypeEnum.SUB) -> List[int]: | |
episodes: List[int] = anime.get_episodes(lang) | |
if episodes: | |
return episodes | |
raise EpisodesListError('No episodes have been found.\n') | |
def get_stream_url(self, anime: Anime, episode=1, lang=LanguageTypeEnum.SUB, quality=720) -> ProviderStream: | |
return anime.get_video(episode=episode, lang=lang, preferred_quality=quality) | |
def get_stream_urls(self, anime: Anime, lang=LanguageTypeEnum.SUB, episode=1) -> List[ProviderStream]: | |
streams: List[ProviderStream] = sorted( | |
anime.get_videos(episode=episode, lang=lang), | |
key=lambda stream: stream.resolution, | |
reverse=True, | |
) | |
if streams: | |
return streams | |
raise NoStreamUrl('No stream URL has been found.') | |
class Search: | |
def __init__(self, provider) -> None: | |
self.provider = provider | |
self.result = dict() | |
def list_search_result(self, search_query: str) -> Dict[str, Any]: | |
assert search_query is not None | |
self.result = { | |
search_result.name: search_result | |
for search_result in self.provider().get_search(search_query) | |
} | |
return self.result | |
def __getitem__(self, key: str) -> Any: | |
return self.result[key] | |
class ListProvider: | |
def __init__(self) -> None: | |
self.providers = {provider.NAME: provider for provider in list_providers()} | |
def __len__(self) -> int: | |
return len(self.providers) | |
def __getitem__(self, key: str) -> str: | |
return self.providers[key] | |
def __contains__(self, key: str) -> str: | |
return key in self.providers | |
class Main: | |
def __init__(self) -> None: | |
self.provider_list = ListProvider() | |
self.__call__() | |
def __call__(self) -> None: | |
self.main_loop() | |
return | |
def main_loop(self) -> None: | |
user_search: str = input(f'{Fore.BLUE}> Search for an anime: {Fore.GREEN}{Style.RESET_ALL}').strip() | |
assert user_search, "Please provide a valid query.\n" | |
if 'gogoanime' in self.provider_list: | |
provider = self.provider_list['gogoanime'] | |
else: | |
provider = self.provider_list['yugenanime'] | |
search_instance = Search(provider=provider) | |
search_results = search_instance.list_search_result(user_search) | |
if not search_results: | |
print(f'{Fore.RED}No results found. {Style.RESET_ALL}\n') | |
return | |
extractor = AnimeExtractor(provider=provider, search_result=search_results) | |
anime_list: Dict[str, Anime] = {anime.name: anime for anime in extractor.get_anime_infos()} | |
user_prompt: List[str] = FzfPrompt().prompt([name for name in anime_list.keys()], '--reverse') | |
assert user_prompt, "Please select an anime from the list." | |
anime = anime_list[user_prompt[0]] | |
assert anime, "Invalid anime selection." | |
episodes_list: List[int] = extractor.list_episodes(anime=anime) | |
episode: int = int(FzfPrompt().prompt(episodes_list, '--reverse')[0]) | |
stream_urls: List[ProviderStream] = extractor.get_stream_urls(anime=anime, episode=episode) | |
player = Player() | |
player.mpv_play_url(stream_urls[0].url, title=f'{user_prompt[0]} - Episode: {episode}') | |
return | |
# Errors | |
class NoSuchPlayerFound(Exception): | |
pass | |
class NoStreamUrl(Exception): | |
pass | |
class EpisodesListError(Exception): | |
pass | |
if __name__ == '__main__': | |
Main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
anipy-api==3.2.3 | |
beautifulsoup4==4.12.3 | |
certifi==2024.8.30 | |
charset-normalizer==3.4.0 | |
colorama==0.4.6 | |
dataclasses-json==0.6.7 | |
idna==3.10 | |
Levenshtein==0.25.1 | |
m3u8==4.1.0 | |
marshmallow==3.23.1 | |
mypy-extensions==1.0.0 | |
packaging==24.2 | |
pycryptodomex==3.21.0 | |
pyee==12.1.1 | |
pyfzf==0.3.1 | |
python-ffmpeg==2.0.12 | |
python-mpv==1.0.7 | |
RapidFuzz==3.10.1 | |
requests==2.32.3 | |
soupsieve==2.6 | |
typing-inspect==0.9.0 | |
typing_extensions==4.12.2 | |
urllib3==2.2.3 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment