Compare commits

..

7 Commits

Author SHA1 Message Date
Benex254
6c1f8d09e6 chore: update package info 2024-07-26 14:13:50 +03:00
Benex254
6bb2c89a8c feat(mpv): improve streaming on mobile 2024-07-26 14:10:49 +03:00
Benex254
9f56b74ff0 feat(utils): add logging 2024-07-26 14:10:12 +03:00
Benex254
4d03b86498 chore(anime_provider): remove print statements from provider and switch to logging 2024-07-26 14:09:44 +03:00
Benex254
fab86090a3 chore: remove legacy code 2024-07-26 14:07:57 +03:00
Benex254
71d258385c chore(constants): create constants module to store useful constants 2024-07-26 14:07:37 +03:00
Benex254
bc55ed6e81 chore(updater): update updater info 2024-07-26 14:04:53 +03:00
17 changed files with 294 additions and 233 deletions

View File

@@ -8,7 +8,7 @@ from subprocess import PIPE, Popen
import requests
from rich import print
from .. import APP_NAME, AUTHOR, GIT_REPO, REPO, __version__
from .. import APP_NAME, AUTHOR, GIT_REPO, __version__
API_URL = f"https://api.{GIT_REPO}/repos/{AUTHOR}/{APP_NAME}/releases/latest"
@@ -91,13 +91,12 @@ def update_app():
else:
executable = sys.executable
app_package_url = f"https://{REPO}/releases/download/{tag_name}/fastanime-{tag_name.replace("v","")}.tar.gz"
args = [
executable,
"-m",
"pip",
"install",
app_package_url,
APP_NAME,
"--user",
"--no-warn-script-location",
]

View File

@@ -2,7 +2,7 @@ import json
import logging
import os
from .. import USER_DATA_PATH
from ..constants import USER_DATA_PATH
logger = logging.getLogger(__name__)

View File

@@ -1,3 +1,4 @@
import logging
import os
import re
import shutil
@@ -10,6 +11,7 @@ from fastanime.libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchem
from .data import anime_normalizer
logger = logging.getLogger(__name__)
# TODO: make it use color_text instead of fixed vals
# from .kivy_markup_helper import color_text
@@ -127,6 +129,7 @@ def anime_title_percentage_match(
fuzz.ratio(title_a.lower(), possible_user_requested_anime_title.lower()),
fuzz.ratio(title_b.lower(), possible_user_requested_anime_title.lower()),
)
logger.info(f"{locals()}")
return percentage_ratio

View File

@@ -1,10 +1,8 @@
import logging
import os
import sys
from platform import platform
from dotenv import load_dotenv
from platformdirs import PlatformDirs
load_dotenv()
@@ -15,43 +13,12 @@ if os.environ.get("FA_RICH_TRACEBACK", False):
# initiate constants
__version__ = "v0.30.0"
__version__ = "v0.32.0"
PLATFORM = platform()
APP_NAME = "FastAnime"
AUTHOR = "Benex254"
GIT_REPO = "github.com"
REPO = f"{GIT_REPO}/{AUTHOR}/{APP_NAME}"
USER_NAME = os.environ.get("USERNAME", f"{APP_NAME} user")
dirs = PlatformDirs(appname=APP_NAME, appauthor=AUTHOR, ensure_exists=True)
# ---- app deps ----
APP_DIR = os.path.abspath(os.path.dirname(__file__))
CONFIGS_DIR = os.path.join(APP_DIR, "configs")
ASSETS_DIR = os.path.join(APP_DIR, "assets")
# ----- user configs and data -----
APP_DATA_DIR = dirs.user_config_dir
if not APP_DATA_DIR:
APP_DATA_DIR = dirs.user_data_dir
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
# cache dir
APP_CACHE_DIR = dirs.user_cache_dir
# video dir
USER_VIDEOS_DIR = os.path.join(dirs.user_videos_dir, APP_NAME)
# web dirs
WEB_DIR = os.path.join(APP_DIR, "web")
FRONTEND_DIR = os.path.join(WEB_DIR, "frontend")
BACKEND_DIR = os.path.join(WEB_DIR, "backend")
def FastAnime():

View File

@@ -4,7 +4,9 @@ import subprocess
import click
from rich import print
from ... import USER_CONFIG_PATH
from fastanime.cli.config import Config
from ...constants import USER_CONFIG_PATH
from ..utils.tools import exit_app
@@ -13,7 +15,8 @@ from ..utils.tools import exit_app
short_help="Edit your config",
)
@click.option("--path", "-p", help="Print the config location and exit", is_flag=True)
def configure(path):
@click.pass_obj
def configure(config: Config, path):
if path:
print(USER_CONFIG_PATH)
else:

View File

@@ -1,5 +1,6 @@
import click
from rich import print
from rich.progress import Progress
from thefuzz import fuzz
from ...libs.anime_provider.types import Anime
@@ -28,9 +29,11 @@ def download(config: Config, anime_title, episode_range):
anime_provider = config.anime_provider
translation_type = config.translation_type
download_dir = config.downloads_dir
search_results = anime_provider.search_for_anime(
anime_title, translation_type=translation_type
)
with Progress() as progress:
progress.add_task("Fetching Search Results...", total=None)
search_results = anime_provider.search_for_anime(
anime_title, translation_type=translation_type
)
if not search_results:
print("Search results failed")
input("Enter to retry")
@@ -51,7 +54,11 @@ def download(config: Config, anime_title, episode_range):
list(search_results_.keys()), "Please Select title: ", "FastAnime"
)
anime: Anime | None = anime_provider.get_anime(search_results_[search_result]["id"])
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
anime: Anime | None = anime_provider.get_anime(
search_results_[search_result]["id"]
)
if not anime:
print("Sth went wring anime no found")
input("Enter to continue...")
@@ -70,20 +77,24 @@ def download(config: Config, anime_title, episode_range):
if episode not in episodes:
print(f"[cyan]Warning[/]: Episode {episode} not found, skipping")
continue
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
print("No streams skipping")
continue
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
print("No streams skipping")
continue
streams = list(streams)
links = [
(link.get("priority", 0), link["link"])
for server in streams
for link in server["links"]
]
link = max(links, key=lambda x: x[0])[1]
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
streams = list(streams)
links = [
(link.get("priority", 0), link["link"])
for server in streams
for link in server["links"]
]
link = max(links, key=lambda x: x[0])[1]
downloader._download_file(
link,
download_dir,

View File

@@ -1,5 +1,6 @@
import click
from rich import print
from rich.progress import Progress
from thefuzz import fuzz
from ...cli.config import Config
@@ -23,9 +24,11 @@ from ..utils.utils import clear
@click.pass_obj
def search(config: Config, anime_title: str, episode_range: str):
anime_provider = config.anime_provider
search_results = anime_provider.search_for_anime(
anime_title, config.translation_type
)
with Progress() as progress:
progress.add_task("Fetching Search Results...", total=None)
search_results = anime_provider.search_for_anime(
anime_title, config.translation_type
)
if not search_results:
print("Search results not found")
input("Enter to retry")
@@ -50,7 +53,12 @@ def search(config: Config, anime_title: str, episode_range: str):
list(search_results_.keys()), "Please Select title: ", "FastAnime"
)
anime: Anime | None = anime_provider.get_anime(search_results_[search_result]["id"])
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
anime: Anime | None = anime_provider.get_anime(
search_results_[search_result]["id"]
)
if not anime:
print("Sth went wring anime no found")
input("Enter to continue...")
@@ -82,17 +90,20 @@ def search(config: Config, anime_title: str, episode_range: str):
if not episode or episode not in episodes:
episode = fzf.run(episodes, "Select an episode: ", header=search_result)
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
print("Failed to get streams")
return
links = [link["link"] for server in streams for link in server["links"]]
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
print("Failed to get streams")
return
links = [link["link"] for server in streams for link in server["links"]]
# TODO: Come up with way to know quality and better server interface
link = links[config.quality]
# TODO: Come up with way to know quality and better server interface
link = links[config.quality]
# link = fzf.run(links, "Select stream", "Streams")
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
mpv(link, search_result)
stream_anime()

View File

@@ -3,8 +3,8 @@ from configparser import ConfigParser
from rich import print
from .. import USER_CONFIG_PATH, USER_VIDEOS_DIR
from ..AnimeProvider import AnimeProvider
from ..constants import USER_CONFIG_PATH, USER_VIDEOS_DIR
from ..Utility.user_data_helper import user_data_helper

View File

@@ -4,10 +4,11 @@ import os
import random
from rich import print
from rich.progress import Progress
from rich.prompt import Prompt
from ... import USER_CONFIG_PATH
from ...anilist import AniList
from ...constants import USER_CONFIG_PATH
from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
from ...libs.anime_provider.types import Anime, SearchResult, Server
from ...libs.fzf import fzf
@@ -154,17 +155,20 @@ def fetch_streams(config: Config, anilist_config: QueryDict):
anime_provider = config.anime_provider
# get streams for episode from provider
episode_streams = anime_provider.get_episode_streams(
anime, episode_number, translation_type
)
if not episode_streams:
print("Failed to fetch :cry:")
input("Enter to retry...")
return fetch_streams(config, anilist_config)
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
episode_streams = anime_provider.get_episode_streams(
anime, episode_number, translation_type
)
if not episode_streams:
print("Failed to fetch :cry:")
input("Enter to retry...")
return fetch_streams(config, anilist_config)
episode_streams = {
episode_stream["server"]: episode_stream for episode_stream in episode_streams
}
episode_streams = {
episode_stream["server"]: episode_stream
for episode_stream in episode_streams
}
# prompt for preferred server
server = None
@@ -267,7 +271,9 @@ def fetch_episode(config: Config, anilist_config: QueryDict):
def fetch_anime_episode(config, anilist_config: QueryDict):
selected_anime: SearchResult = anilist_config._anime
anime_provider = config.anime_provider
anilist_config.anime = anime_provider.get_anime(selected_anime["id"])
with Progress() as progress:
progress.add_task("Fetching Anime Info...", total=None)
anilist_config.anime = anime_provider.get_anime(selected_anime["id"])
if not anilist_config.anime:
print(
@@ -291,9 +297,11 @@ def provide_anime(config: Config, anilist_config: QueryDict):
anime_provider = config.anime_provider
# search and get the requested title from provider
search_results = anime_provider.search_for_anime(
selected_anime_title, translation_type
)
with Progress() as progress:
progress.add_task("Fetching Search Results...", total=None)
search_results = anime_provider.search_for_anime(
selected_anime_title, translation_type
)
if not search_results:
print(
"Sth went wrong :cry: while fetching this could mean you have poor internet connection or the provider is down"

View File

@@ -5,7 +5,7 @@ from threading import Thread
import requests
from ... import APP_CACHE_DIR
from ...constants import APP_CACHE_DIR
from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
from ...Utility import anilist_data_helper
from ...Utility.utils import remove_html_tags, sanitize_filename

View File

@@ -1,23 +1,87 @@
import re
import shutil
import subprocess
from typing import Optional
def mpv(link, title: None | str = "anime", *custom_args):
# legacy
# def mpv(link, title: None | str = "anime", *custom_args):
# MPV = shutil.which("mpv")
# if not MPV:
# args = [
# "nohup",
# "am",
# "start",
# "--user",
# "0",
# "-a",
# "android.intent.action.VIEW",
# "-d",
# link,
# "-n",
# "is.xyz.mpv/.MPVActivity",
# ]
# subprocess.run(args)
# else:
# subprocess.run([MPV, *custom_args, f"--title={title}", link])
#
#
def mpv(link: str, title: Optional[str] = "anime", *custom_args):
# Determine if mpv is available
MPV = shutil.which("mpv")
# If title is None, set a default value
if title is None:
title = "anime"
# Regex to check if the link is a YouTube URL
youtube_regex = r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/.+"
if not MPV:
args = [
"nohup",
"am",
"start",
"--user",
"0",
"-a",
"android.intent.action.VIEW",
"-d",
link,
"-n",
"is.xyz.mpv/.MPVActivity",
]
# Determine if the link is a YouTube URL
if re.match(youtube_regex, link):
# Android specific commands to launch mpv with a YouTube URL
args = [
"nohup",
"am",
"start",
"--user",
"0",
"-a",
"android.intent.action.VIEW",
"-d",
link,
"-n",
"com.google.android.youtube/.UrlActivity",
]
else:
# Android specific commands to launch mpv with a regular URL
args = [
"nohup",
"am",
"start",
"--user",
"0",
"-a",
"android.intent.action.VIEW",
"-d",
link,
"-n",
"is.xyz.mpv/.MPVActivity",
]
subprocess.run(args)
else:
subprocess.run([MPV, *custom_args, f"--title={title}", link])
# General mpv command with custom arguments
mpv_args = [MPV, *custom_args, f"--title={title}", link]
subprocess.run(mpv_args)
# Example usage
if __name__ == "__main__":
mpv(
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"Example Video",
"--fullscreen",
"--volume=50",
)

View File

@@ -18,7 +18,7 @@ def exit_app(*args):
from rich import print
from ... import USER_NAME
from ...constants import USER_NAME
print("Have a good day :smile:", USER_NAME)
sys.exit(0)

View File

@@ -4,7 +4,7 @@ import os
from InquirerPy import inquirer
from thefuzz import fuzz
from ... import PLATFORM
from ...constants import PLATFORM
from ...Utility.data import anime_normalizer
logger = logging.getLogger(__name__)
@@ -54,20 +54,3 @@ def anime_title_percentage_match(
fuzz.ratio(title[1].lower(), possible_user_requested_anime_title.lower()),
)
return percentage_ratio
def get_selected_anime(anime_title, results):
def _get_result(result, compare):
return result["name"] == compare
return list(
filter(lambda x: _get_result(x, anime_title), results["shows"]["edges"])
)
def get_selected_server(_server, servers):
def _get_server(server, server_name):
return server[0] == server_name
server = list(filter(lambda x: _get_server(x, _server), servers)).pop()
return server

32
fastanime/constants.py Normal file
View File

@@ -0,0 +1,32 @@
import os
from platform import platform
from platformdirs import PlatformDirs
from . import APP_NAME, AUTHOR
PLATFORM = platform()
dirs = PlatformDirs(appname=APP_NAME, appauthor=AUTHOR, ensure_exists=True)
# ---- app deps ----
APP_DIR = os.path.abspath(os.path.dirname(__file__))
CONFIGS_DIR = os.path.join(APP_DIR, "configs")
ASSETS_DIR = os.path.join(APP_DIR, "assets")
# ----- user configs and data -----
APP_DATA_DIR = dirs.user_config_dir
if not APP_DATA_DIR:
APP_DATA_DIR = dirs.user_data_dir
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
# cache dir
APP_CACHE_DIR = dirs.user_cache_dir
# video dir
USER_VIDEOS_DIR = os.path.join(dirs.user_videos_dir, APP_NAME)
USER_NAME = os.environ.get("USERNAME", f"{APP_NAME} user")

View File

@@ -4,8 +4,6 @@ from typing import Iterator
import requests
from requests.exceptions import Timeout
from rich import print
from rich.progress import Progress
from ....libs.anime_provider.allanime.types import AllAnimeEpisode
from ....libs.anime_provider.types import Anime, Server
@@ -43,14 +41,12 @@ class AllAnimeAPI:
timeout=10,
)
return response.json()["data"]
except Timeout as e:
print(
"Timeout has been exceeded :cry:. This could mean allanime is down or your internet is down"
except Timeout:
Logger.error(
"allanime(Error):Timeout exceeded this could mean allanime is down or you have lost internet connection"
)
Logger.error(f"allanime(Error): {e}")
return {}
except Exception as e:
print("sth went wrong :confused:")
Logger.error(f"allanime:Error: {e}")
return {}
@@ -75,22 +71,20 @@ class AllAnimeAPI:
"countryorigin": countryorigin,
}
try:
with Progress() as progress:
progress.add_task("[cyan]searching..", start=False, total=None)
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
return normalize_search_results(search_results) # pyright:ignore
except Exception:
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
return normalize_search_results(search_results) # pyright:ignore
except Exception as e:
Logger.error(f"FA(AllAnime): {e}")
return {}
def get_anime(self, allanime_show_id: str):
variables = {"showId": allanime_show_id}
try:
with Progress() as progress:
progress.add_task("[cyan]fetching anime..", start=False, total=None)
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
return normalize_anime(anime["show"])
except Exception:
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
return normalize_anime(anime["show"])
except Exception as e:
Logger.error(f"FA(AllAnime): {e}")
return None
def get_anime_episode(
@@ -102,11 +96,10 @@ class AllAnimeAPI:
"episodeString": episode_string,
}
try:
with Progress() as progress:
progress.add_task("[cyan]fetching episode..", start=False, total=None)
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
return episode["episode"] # pyright: ignore
except Exception:
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
return episode["episode"] # pyright: ignore
except Exception as e:
Logger.error(f"FA(AllAnime): {e}")
return {}
def get_episode_streams(
@@ -121,99 +114,86 @@ class AllAnimeAPI:
embeds = allanime_episode["sourceUrls"]
try:
with Progress() as progress:
progress.add_task("[cyan]fetching streams..", start=False, total=None)
for embed in embeds:
try:
# filter the working streams
if embed.get("sourceName", "") not in (
"Sak",
"Kir",
"S-mp4",
"Luf-mp4",
):
continue
url = embed.get("sourceUrl")
for embed in embeds:
try:
# filter the working streams
if embed.get("sourceName", "") not in (
"Sak",
"Kir",
"S-mp4",
"Luf-mp4",
):
continue
url = embed.get("sourceUrl")
if not url:
continue
if url.startswith("--"):
url = url[2:]
if not url:
continue
if url.startswith("--"):
url = url[2:]
# get the stream url for an episode of the defined source names
parsed_url = decode_hex_string(url)
embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock','clock.json')}"
resp = requests.get(
embed_url,
headers={
"Referer": ALLANIME_REFERER,
"User-Agent": USER_AGENT,
},
timeout=10,
)
if resp.status_code == 200:
match embed["sourceName"]:
case "Luf-mp4":
Logger.debug(
"allanime:Found streams from gogoanime"
# get the stream url for an episode of the defined source names
parsed_url = decode_hex_string(url)
embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock','clock.json')}"
resp = requests.get(
embed_url,
headers={
"Referer": ALLANIME_REFERER,
"User-Agent": USER_AGENT,
},
timeout=10,
)
if resp.status_code == 200:
match embed["sourceName"]:
case "Luf-mp4":
Logger.debug("allanime:Found streams from gogoanime")
yield {
"server": "gogoanime",
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
print("[yellow]GogoAnime Fetched")
yield {
"server": "gogoanime",
"episode_title": (
allanime_episode["notes"]
or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "Kir":
Logger.debug(
"allanime:Found streams from wetransfer"
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "Kir":
Logger.debug("allanime:Found streams from wetransfer")
yield {
"server": "wetransfer",
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
print("[yellow]WeTransfer Fetched")
yield {
"server": "wetransfer",
"episode_title": (
allanime_episode["notes"]
or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "S-mp4":
Logger.debug(
"allanime:Found streams from sharepoint"
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "S-mp4":
Logger.debug("allanime:Found streams from sharepoint")
yield {
"server": "sharepoint",
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
print("[yellow]Sharepoint Fetched")
yield {
"server": "sharepoint",
"episode_title": (
allanime_episode["notes"]
or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "Sak":
Logger.debug("allanime:Found streams from dropbox")
print("[yellow]Dropbox Fetched")
yield {
"server": "dropbox",
"episode_title": (
allanime_episode["notes"]
or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
except Timeout:
print(
"Timeout has been exceeded :cry: this could mean allanime is down or your internet connection is poor"
)
except Exception as e:
print("Sth went wrong :confused:", e)
except Exception:
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "Sak":
Logger.debug("allanime:Found streams from dropbox")
yield {
"server": "dropbox",
"episode_title": (
allanime_episode["notes"] or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
except Timeout:
Logger.error(
"Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
)
return []
except Exception as e:
Logger.error(f"FA(Allanime): {e}")
return []
except Exception as e:
Logger.error(f"FA(Allanime): {e}")
return []

View File

@@ -8,7 +8,7 @@ from typing import Callable, List
from art import text2art
from rich import print
from ... import PLATFORM
from ...constants import PLATFORM
from .config import FZF_DEFAULT_OPTS, FzfOptions
logger = logging.getLogger(__name__)

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "fastanime"
version = "0.31.3"
version = "0.32.0"
description = "A fast and efficient anime scrapper and exploration tool"
authors = ["Benex254 <benedictx855@gmail.com>"]
license = "UNLICENSE"