mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-10 06:40:39 -08:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
452c2a3569 | ||
|
|
f738069794 | ||
|
|
d178eb976e | ||
|
|
d58dae6d6b | ||
|
|
136cf841e1 | ||
|
|
748d321f36 | ||
|
|
3e71239981 | ||
|
|
571ab488f8 | ||
|
|
62878311c6 | ||
|
|
432e9374cb | ||
|
|
a4974fbba7 | ||
|
|
b935e80928 | ||
|
|
8999d88d23 | ||
|
|
9608bace07 | ||
|
|
09b88df49a | ||
|
|
ccaacc9948 | ||
|
|
96d88b0f47 | ||
|
|
6939471e48 | ||
|
|
b1a2307c4d | ||
|
|
1ead2fb176 | ||
|
|
51e3ca004e | ||
|
|
3814db460f | ||
|
|
4102685cbc | ||
|
|
c80a8235e1 | ||
|
|
622427b748 | ||
|
|
e36413cdef | ||
|
|
7e9a510706 | ||
|
|
9185a08102 | ||
|
|
4f754a5129 | ||
|
|
cec7aaebdb | ||
|
|
bfd580ec79 | ||
|
|
aabd356c0b | ||
|
|
f929e83a62 | ||
|
|
74cacded6e | ||
|
|
82b6b849cf | ||
|
|
5dbc3a16f7 | ||
|
|
912535d166 |
24
README.md
24
README.md
@@ -44,6 +44,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
|
||||
- [downloads subcommand](#downloads-subcommand)
|
||||
- [config subcommand](#config-subcommand)
|
||||
- [cache subcommand](#cache-subcommand)
|
||||
- [update subcommand](#update-subcommand)
|
||||
- [completions subcommand](#completions-subcommand)
|
||||
- [MPV specific commands](#mpv-specific-commands)
|
||||
- [Added keybindings](#added-keybindings)
|
||||
@@ -309,15 +310,15 @@ Powerful command mainly aimed at binging anime. Since it doesn't require interac
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# basic form where you will still be promted for the episode number
|
||||
# basic form where you will still be prompted for the episode number
|
||||
fastanime search <anime-title>
|
||||
|
||||
# binge all episodes with this command
|
||||
fastanime search <anime-title> -
|
||||
fastanime search <anime-title> -r -
|
||||
|
||||
# binge a specific episode range with this command
|
||||
# be sure to observe the range Syntax
|
||||
fastanime search <anime-title> <episodes-start>-<episodes-end>
|
||||
fastanime search <anime-title> -r <episodes-start>-<episodes-end>
|
||||
```
|
||||
|
||||
#### downloads subcommand
|
||||
@@ -348,6 +349,9 @@ fastanime config --path
|
||||
|
||||
# add a desktop entry
|
||||
fastanime config --desktop-entry
|
||||
|
||||
# view current contents of your configuration or can be used to get an example config
|
||||
fastanime config --view
|
||||
```
|
||||
|
||||
> [!Note]
|
||||
@@ -374,6 +378,20 @@ fastanime cache --size
|
||||
fastanime cache
|
||||
```
|
||||
|
||||
#### update subcommand
|
||||
|
||||
Easily update fastanime to latest
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# update fastanime to latest
|
||||
fastanime update
|
||||
|
||||
# check for latest release
|
||||
fastanime update --check
|
||||
```
|
||||
|
||||
#### completions subcommand
|
||||
|
||||
Helper command to setup shell completions
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""This package exist as away to expose functions and classes that my be useful to a developer using the fastanime library
|
||||
|
||||
[TODO:description]
|
||||
"""
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..libs.anilist.types import AnilistDateObject, AnilistMediaNextAiringEpisode
|
||||
if TYPE_CHECKING:
|
||||
from ..libs.anilist.types import AnilistDateObject, AnilistMediaNextAiringEpisode
|
||||
|
||||
|
||||
# TODO: Add formating options for the final date
|
||||
def format_anilist_date_object(anilist_date_object: AnilistDateObject):
|
||||
def format_anilist_date_object(anilist_date_object: "AnilistDateObject"):
|
||||
if anilist_date_object:
|
||||
return f"{anilist_date_object['day']}/{anilist_date_object['month']}/{anilist_date_object['year']}"
|
||||
else:
|
||||
@@ -25,7 +27,7 @@ def format_list_data_with_comma(data: list | None):
|
||||
return "None"
|
||||
|
||||
|
||||
def extract_next_airing_episode(airing_episode: AnilistMediaNextAiringEpisode):
|
||||
def extract_next_airing_episode(airing_episode: "AnilistMediaNextAiringEpisode"):
|
||||
if airing_episode:
|
||||
return f"{airing_episode['episode']} on {format_anilist_timestamp(airing_episode['airingAt'])}"
|
||||
else:
|
||||
|
||||
@@ -27,9 +27,27 @@ class YtDLPDownloader:
|
||||
|
||||
# Function to download the file
|
||||
# TODO: untpack the title to its actual values episode_title and anime_title
|
||||
def _download_file(self, url: str, download_dir, title, silent, vid_format="best"):
|
||||
anime_title = sanitize_filename(title[0])
|
||||
episode_title = sanitize_filename(title[1])
|
||||
def _download_file(
|
||||
self,
|
||||
url: str,
|
||||
anime_title: str,
|
||||
episode_title: str,
|
||||
download_dir: str,
|
||||
silent: bool,
|
||||
vid_format: str = "best",
|
||||
):
|
||||
"""Helper function that downloads anime given url and path details
|
||||
|
||||
Args:
|
||||
url: [TODO:description]
|
||||
anime_title: [TODO:description]
|
||||
episode_title: [TODO:description]
|
||||
download_dir: [TODO:description]
|
||||
silent: [TODO:description]
|
||||
vid_format: [TODO:description]
|
||||
"""
|
||||
anime_title = sanitize_filename(anime_title)
|
||||
episode_title = sanitize_filename(episode_title)
|
||||
ydl_opts = {
|
||||
# Specify the output path and template
|
||||
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s",
|
||||
@@ -41,7 +59,15 @@ class YtDLPDownloader:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
|
||||
# WARN: May remove this legacy functionality
|
||||
def download_file(self, url: str, title, silent=True):
|
||||
"""A helper that just does things in the background
|
||||
|
||||
Args:
|
||||
title ([TODO:parameter]): [TODO:description]
|
||||
silent ([TODO:parameter]): [TODO:description]
|
||||
url: [TODO:description]
|
||||
"""
|
||||
self.downloads_queue.put((self._download_file, (url, title, silent)))
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from ..constants import USER_DATA_PATH
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: merger this functionality with the config object
|
||||
class UserData:
|
||||
user_data = {"watch_history": {}, "animelist": [], "user": {}}
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
if os.path.isfile(USER_DATA_PATH):
|
||||
with open(USER_DATA_PATH, "r") as f:
|
||||
user_data = json.load(f)
|
||||
self.user_data.update(user_data)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def update_watch_history(self, watch_history: dict):
|
||||
self.user_data["watch_history"] = watch_history
|
||||
self._update_user_data()
|
||||
|
||||
def update_user_info(self, user: dict):
|
||||
self.user_data["user"] = user
|
||||
self._update_user_data()
|
||||
|
||||
def update_animelist(self, anime_list: list):
|
||||
self.user_data["animelist"] = list(set(anime_list))
|
||||
self._update_user_data()
|
||||
|
||||
def _update_user_data(self):
|
||||
with open(USER_DATA_PATH, "w") as f:
|
||||
json.dump(self.user_data, f)
|
||||
|
||||
|
||||
user_data_helper = UserData()
|
||||
@@ -1,79 +1,18 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from thefuzz import fuzz
|
||||
|
||||
from fastanime.libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
|
||||
from .data import anime_normalizer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def remove_html_tags(text: str):
|
||||
clean = re.compile("<.*?>")
|
||||
return re.sub(clean, "", text)
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def sanitize_filename(filename: str):
|
||||
"""
|
||||
Sanitize a string to be safe for use as a file name.
|
||||
|
||||
:param filename: The original filename string.
|
||||
:return: A sanitized filename string.
|
||||
"""
|
||||
# List of characters not allowed in filenames on various operating systems
|
||||
invalid_chars = r'[<>:"/\\|?*\0]'
|
||||
reserved_names = {
|
||||
"CON",
|
||||
"PRN",
|
||||
"AUX",
|
||||
"NUL",
|
||||
"COM1",
|
||||
"COM2",
|
||||
"COM3",
|
||||
"COM4",
|
||||
"COM5",
|
||||
"COM6",
|
||||
"COM7",
|
||||
"COM8",
|
||||
"COM9",
|
||||
"LPT1",
|
||||
"LPT2",
|
||||
"LPT3",
|
||||
"LPT4",
|
||||
"LPT5",
|
||||
"LPT6",
|
||||
"LPT7",
|
||||
"LPT8",
|
||||
"LPT9",
|
||||
}
|
||||
|
||||
# Replace invalid characters with an underscore
|
||||
sanitized = re.sub(invalid_chars, " ", filename)
|
||||
|
||||
# Remove leading and trailing whitespace
|
||||
sanitized = sanitized.strip()
|
||||
|
||||
# Check for reserved filenames
|
||||
name, ext = os.path.splitext(sanitized)
|
||||
if name.upper() in reserved_names:
|
||||
name += "_file"
|
||||
sanitized = name + ext
|
||||
|
||||
# Ensure the filename is not empty
|
||||
if not sanitized:
|
||||
sanitized = "default_filename"
|
||||
|
||||
return sanitized
|
||||
|
||||
|
||||
def anime_title_percentage_match(
|
||||
possible_user_requested_anime_title: str, anime: AnilistBaseMediaDataSchema
|
||||
possible_user_requested_anime_title: str, anime: "AnilistBaseMediaDataSchema"
|
||||
) -> float:
|
||||
"""Returns the percentage match between the possible title and user title
|
||||
|
||||
@@ -97,10 +36,3 @@ def anime_title_percentage_match(
|
||||
)
|
||||
logger.info(f"{locals()}")
|
||||
return percentage_ratio
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
unsafe_filename = "CON:example?file*name.txt"
|
||||
safe_filename = sanitize_filename(unsafe_filename)
|
||||
print(safe_filename) # Output: 'CON_example_file_name.txt'
|
||||
|
||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
||||
) # noqa: F541
|
||||
|
||||
|
||||
__version__ = "v0.62.0"
|
||||
__version__ = "v1.1.0"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
|
||||
@@ -15,6 +15,7 @@ commands = {
|
||||
"downloads": "downloads.downloads",
|
||||
"cache": "cache.cache",
|
||||
"completions": "completions.completions",
|
||||
"update": "update.update",
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +43,6 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
@click.option("--log", help="Allow logging to stdout", is_flag=True)
|
||||
@click.option("--log-file", help="Allow logging to a file", is_flag=True)
|
||||
@click.option("--rich-traceback", help="Use rich to output tracebacks", is_flag=True)
|
||||
@click.option("--update", help="Update fastanime to the latest version", is_flag=True)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--provider",
|
||||
@@ -135,7 +135,6 @@ def run_cli(
|
||||
log,
|
||||
log_file,
|
||||
rich_traceback,
|
||||
update,
|
||||
provider,
|
||||
server,
|
||||
format,
|
||||
@@ -192,11 +191,6 @@ def run_cli(
|
||||
from rich.traceback import install
|
||||
|
||||
install()
|
||||
if update and None:
|
||||
from .app_updater import update_app
|
||||
|
||||
update_app()
|
||||
return
|
||||
|
||||
if provider:
|
||||
ctx.obj.provider = provider
|
||||
|
||||
@@ -2,8 +2,8 @@ import pathlib
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from subprocess import PIPE, Popen
|
||||
|
||||
import requests
|
||||
from rich import print
|
||||
@@ -34,71 +34,72 @@ def check_for_updates():
|
||||
|
||||
def is_git_repo(author, repository):
|
||||
# Check if the current directory contains a .git folder
|
||||
if not pathlib.Path("./.git").exists():
|
||||
git_dir = pathlib.Path(".git")
|
||||
if not git_dir.exists() or not git_dir.is_dir():
|
||||
return False
|
||||
|
||||
repository_qualname = f"{author}/{repository}"
|
||||
|
||||
# Read the .git/config file to find the remote repository URL
|
||||
config_path = pathlib.Path("./.git/config")
|
||||
# Check if the config file exists
|
||||
config_path = git_dir / "config"
|
||||
if not config_path.exists():
|
||||
return False
|
||||
print("here")
|
||||
|
||||
with open(config_path, "r") as git_config:
|
||||
git_config_content = git_config.read()
|
||||
|
||||
# Use regex to find the repository URL in the config file
|
||||
repo_name_pattern = r"\[remote \"origin\"\]\s+url = .*\/([^/]+\/[^/]+)\.git"
|
||||
match = re.search(repo_name_pattern, git_config_content)
|
||||
print(match)
|
||||
|
||||
if match is None:
|
||||
try:
|
||||
# Read the .git/config file to find the remote repository URL
|
||||
with config_path.open("r") as git_config:
|
||||
git_config_content = git_config.read()
|
||||
except (FileNotFoundError, PermissionError):
|
||||
return False
|
||||
|
||||
# Extract the repository name and compare with the expected repository_qualname
|
||||
config_repo_name = match.group(1)
|
||||
return config_repo_name == repository_qualname
|
||||
# Use regex to find the repository URL in the config file
|
||||
repo_name_pattern = r"url\s*=\s*.+/([^/]+/[^/]+)\.git"
|
||||
match = re.search(repo_name_pattern, git_config_content)
|
||||
|
||||
# Return True if match found and repository name matches
|
||||
return bool(match) and match.group(1) == f"{author}/{repository}"
|
||||
|
||||
|
||||
def update_app():
|
||||
is_latest, release_json = check_for_updates()
|
||||
if is_latest:
|
||||
print("[green]App is up to date[/]")
|
||||
return
|
||||
return False, release_json
|
||||
tag_name = release_json["tag_name"]
|
||||
|
||||
print("[cyan]Updating app to version %s[/]" % tag_name)
|
||||
if is_git_repo(AUTHOR, APP_NAME):
|
||||
executable = shutil.which("git")
|
||||
GIT_EXECUTABLE = shutil.which("git")
|
||||
args = [
|
||||
executable,
|
||||
GIT_EXECUTABLE,
|
||||
"pull",
|
||||
]
|
||||
|
||||
print(f"Pulling latest changes from the repository via git: {shlex.join(args)}")
|
||||
|
||||
if not executable:
|
||||
return print("[red]Cannot find git.[/]")
|
||||
if not GIT_EXECUTABLE:
|
||||
print("[red]Cannot find git please install it.[/]")
|
||||
return False, release_json
|
||||
|
||||
process = Popen(
|
||||
process = subprocess.run(
|
||||
args,
|
||||
stdout=PIPE,
|
||||
stderr=PIPE,
|
||||
)
|
||||
|
||||
process.communicate()
|
||||
else:
|
||||
executable = sys.executable
|
||||
if PIPX_EXECUTABLE := shutil.which("pipx"):
|
||||
process = subprocess.run([PIPX_EXECUTABLE, "upgrade", APP_NAME])
|
||||
else:
|
||||
PYTHON_EXECUTABLE = sys.executable
|
||||
|
||||
args = [
|
||||
executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
APP_NAME,
|
||||
"--user",
|
||||
"--no-warn-script-location",
|
||||
]
|
||||
process = Popen(args)
|
||||
process.communicate()
|
||||
args = [
|
||||
PYTHON_EXECUTABLE,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
APP_NAME,
|
||||
"--user",
|
||||
"--no-warn-script-location",
|
||||
]
|
||||
process = subprocess.run(args)
|
||||
if process.returncode == 0:
|
||||
return True, release_json
|
||||
else:
|
||||
return False, release_json
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import click
|
||||
|
||||
from ...utils.tools import QueryDict
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
from .__lazyloader__ import LazyGroup
|
||||
|
||||
commands = {
|
||||
@@ -36,7 +36,9 @@ def anilist(ctx: click.Context):
|
||||
|
||||
from ....anilist import AniList
|
||||
from ....AnimeProvider import AnimeProvider
|
||||
from ...interfaces.anilist_interfaces import anilist as anilist_interface
|
||||
from ...interfaces.anilist_interfaces import (
|
||||
fastanime_main_menu as anilist_interface,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...config import Config
|
||||
@@ -45,5 +47,5 @@ def anilist(ctx: click.Context):
|
||||
if user := ctx.obj.user:
|
||||
AniList.update_login_info(user, user["token"])
|
||||
if ctx.invoked_subcommand is None:
|
||||
anilist_config = QueryDict()
|
||||
anilist_interface(ctx.obj, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
anilist_interface(ctx.obj, fastanime_runtime_state)
|
||||
|
||||
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
def completed(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import QueryDict, exit_app
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
@@ -27,6 +27,6 @@ def completed(config: "Config"):
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
def dropped(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import QueryDict, exit_app
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
@@ -27,6 +27,6 @@ def dropped(config: "Config"):
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -8,11 +8,11 @@ import click
|
||||
@click.pass_obj
|
||||
def favourites(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import select_anime
|
||||
from ...utils.tools import QueryDict
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
anime_data = AniList.get_most_favourite()
|
||||
if anime_data[0]:
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_data[1]
|
||||
select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
def paused(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import QueryDict, exit_app
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
@@ -27,6 +27,6 @@ def paused(config: "Config"):
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config = FastAnimeRuntimeState()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
anilist_interfaces.anilist_results_menu(config, anilist_config)
|
||||
|
||||
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
def planning(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import QueryDict, exit_app
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
@@ -27,6 +27,6 @@ def planning(config: "Config"):
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -7,11 +7,11 @@ import click
|
||||
@click.pass_obj
|
||||
def popular(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import select_anime
|
||||
from ...utils.tools import QueryDict
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
anime_data = AniList.get_most_popular()
|
||||
if anime_data[0]:
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_data[1]
|
||||
select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -10,8 +10,8 @@ def random_anime(config):
|
||||
import random
|
||||
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import select_anime
|
||||
from ...utils.tools import QueryDict
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
random_anime = range(1, 15000)
|
||||
|
||||
@@ -20,8 +20,8 @@ def random_anime(config):
|
||||
anime_data = AniList.search(id_in=list(random_anime))
|
||||
|
||||
if anime_data[0]:
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_data[1]
|
||||
select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
print(anime_data[1])
|
||||
|
||||
@@ -8,11 +8,11 @@ import click
|
||||
@click.pass_obj
|
||||
def recent(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import select_anime
|
||||
from ...utils.tools import QueryDict
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
anime_data = AniList.get_most_recently_updated()
|
||||
if anime_data[0]:
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_data[1]
|
||||
select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
def rewatching(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import QueryDict, exit_app
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
@@ -27,6 +27,6 @@ def rewatching(config: "Config"):
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -7,11 +7,11 @@ import click
|
||||
@click.pass_obj
|
||||
def scores(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import select_anime
|
||||
from ...utils.tools import QueryDict
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
anime_data = AniList.get_most_scored()
|
||||
if anime_data[0]:
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_data[1]
|
||||
select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -11,11 +11,11 @@ import click
|
||||
@click.pass_obj
|
||||
def search(config, title):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import select_anime
|
||||
from ...utils.tools import QueryDict
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
success, search_results = AniList.search(title)
|
||||
if success:
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = search_results
|
||||
select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = search_results
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -8,11 +8,11 @@ import click
|
||||
@click.pass_obj
|
||||
def trending(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import select_anime
|
||||
from ...utils.tools import QueryDict
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
success, data = AniList.get_trending()
|
||||
if success:
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = data
|
||||
select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = data
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -7,11 +7,11 @@ import click
|
||||
@click.pass_obj
|
||||
def upcoming(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import select_anime
|
||||
from ...utils.tools import QueryDict
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
success, data = AniList.get_upcoming_anime()
|
||||
if success:
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = data
|
||||
select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = data
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
def watching(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import QueryDict, exit_app
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
@@ -27,6 +27,6 @@ def watching(config: "Config"):
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -25,14 +25,14 @@ def cache(clean, path, size):
|
||||
elif size:
|
||||
import os
|
||||
|
||||
from ..utils.utils import sizeof_fmt
|
||||
from ..utils.utils import format_bytes_to_human
|
||||
|
||||
total_size = 0
|
||||
for dirpath, dirnames, filenames in os.walk(APP_CACHE_DIR):
|
||||
for f in filenames:
|
||||
fp = os.path.join(dirpath, f)
|
||||
total_size += os.path.getsize(fp)
|
||||
print("Total Size: ", sizeof_fmt(total_size))
|
||||
print("Total Size: ", format_bytes_to_human(total_size))
|
||||
else:
|
||||
import click
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import click
|
||||
|
||||
|
||||
@click.command(help="Helper command to get shell completions")
|
||||
@click.option("--fish", is_flag=True)
|
||||
@click.option("--zsh", is_flag=True)
|
||||
@click.option("--bash", is_flag=True)
|
||||
@click.option("--fish", is_flag=True, help="print fish completions")
|
||||
@click.option("--zsh", is_flag=True, help="print zsh completions")
|
||||
@click.option("--bash", is_flag=True, help="print bash completions")
|
||||
def completions(fish, zsh, bash):
|
||||
if not fish or not zsh or not bash:
|
||||
import os
|
||||
|
||||
@@ -1,47 +1,92 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..config import Config
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Opens up your fastanime config in your preferred editor",
|
||||
short_help="Edit your config",
|
||||
)
|
||||
@click.option("--path", "-p", help="Print the config location and exit", is_flag=True)
|
||||
@click.option(
|
||||
"--view", "-v", help="View the current contents of your config", is_flag=True
|
||||
)
|
||||
@click.option(
|
||||
"--desktop-entry",
|
||||
"-d",
|
||||
help="Configure the desktop entry of fastanime",
|
||||
is_flag=True,
|
||||
)
|
||||
# @click.pass_obj
|
||||
def config(path, desktop_entry):
|
||||
pass
|
||||
@click.pass_obj
|
||||
def config(config: "Config", path, view, desktop_entry):
|
||||
import sys
|
||||
|
||||
from pyshortcuts import make_shortcut
|
||||
from rich import print
|
||||
|
||||
from ...constants import APP_NAME, ICON_PATH, USER_CONFIG_PATH
|
||||
from ... import __version__
|
||||
from ...constants import APP_NAME, ICON_PATH, S_PLATFORM, USER_CONFIG_PATH
|
||||
|
||||
if path:
|
||||
print(USER_CONFIG_PATH)
|
||||
elif view:
|
||||
print(config)
|
||||
elif desktop_entry:
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
from rich import print
|
||||
from rich.prompt import Confirm
|
||||
|
||||
from ..utils.tools import exit_app
|
||||
|
||||
FASTANIME_EXECUTABLE = shutil.which("fastanime")
|
||||
if FASTANIME_EXECUTABLE:
|
||||
cmds = f"{FASTANIME_EXECUTABLE} --rofi anilist"
|
||||
else:
|
||||
cmds = "_ -m fastanime --rofi anilist"
|
||||
shortcut = make_shortcut(
|
||||
name=APP_NAME,
|
||||
description="Watch Anime from the terminal",
|
||||
icon=ICON_PATH,
|
||||
script=cmds,
|
||||
terminal=False,
|
||||
)
|
||||
if shortcut:
|
||||
print("Success", shortcut)
|
||||
cmds = f"{sys.executable} -m fastanime --rofi anilist"
|
||||
|
||||
# TODO: Get funs of the other platforms to complete this lol
|
||||
if S_PLATFORM == "win32":
|
||||
print(
|
||||
"Not implemented; the author thinks its not straight forward so welcomes lovers of windows to try and implement it themselves or to switch to a proper os like arch linux or pray the author gets bored 😜"
|
||||
)
|
||||
elif S_PLATFORM == "darwin":
|
||||
print(
|
||||
"Not implemented; the author thinks its not straight forward so welcomes lovers of mac to try and implement it themselves or to switch to a proper os like arch linux or pray the author gets bored 😜"
|
||||
)
|
||||
else:
|
||||
print("Failed")
|
||||
desktop_entry = dedent(
|
||||
f"""
|
||||
[Desktop Entry]
|
||||
Name={APP_NAME}
|
||||
Type=Application
|
||||
version={__version__}
|
||||
Path={Path().home()}
|
||||
Comment=Watch anime from your terminal
|
||||
Terminal=false
|
||||
Icon={ICON_PATH}
|
||||
Exec={cmds}
|
||||
Categories=Entertainment
|
||||
"""
|
||||
)
|
||||
base = os.path.expanduser("~/.local/share/applications")
|
||||
desktop_entry_path = os.path.join(base, f"{APP_NAME}.desktop")
|
||||
if os.path.exists(desktop_entry_path):
|
||||
if not Confirm.ask(
|
||||
f"The file already exists {desktop_entry_path}; or would you like to rewrite it",
|
||||
default=False,
|
||||
):
|
||||
exit_app(1)
|
||||
with open(desktop_entry_path, "w") as f:
|
||||
f.write(desktop_entry)
|
||||
with open(desktop_entry_path) as f:
|
||||
print(f"Successfully wrote \n{f.read()}")
|
||||
exit_app(0)
|
||||
else:
|
||||
import click
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
@@ -70,7 +71,10 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
|
||||
if config.use_fzf:
|
||||
search_result = fzf.run(choices, "Please Select title: ", "FastAnime")
|
||||
else:
|
||||
search_result = fuzzy_inquirer("Please Select title", choices)
|
||||
search_result = fuzzy_inquirer(
|
||||
choices,
|
||||
"Please Select title",
|
||||
)
|
||||
|
||||
# ---- fetch anime ----
|
||||
with Progress() as progress:
|
||||
@@ -125,7 +129,10 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
|
||||
if config.use_fzf:
|
||||
server = fzf.run(servers_names, "Select an link: ")
|
||||
else:
|
||||
server = fuzzy_inquirer("Select link", servers_names)
|
||||
server = fuzzy_inquirer(
|
||||
servers_names,
|
||||
"Select link",
|
||||
)
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, servers[server]["links"]
|
||||
)
|
||||
@@ -139,14 +146,16 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
|
||||
|
||||
downloader._download_file(
|
||||
link,
|
||||
anime["title"],
|
||||
episode_title,
|
||||
download_dir,
|
||||
(anime["title"], episode_title),
|
||||
True,
|
||||
config.format,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
time.sleep(1)
|
||||
print("Continuing")
|
||||
clear()
|
||||
print("Done")
|
||||
print("Done Downloading")
|
||||
exit_app()
|
||||
|
||||
@@ -36,7 +36,10 @@ def downloads(config: "Config", path: bool):
|
||||
elif config.use_rofi:
|
||||
playlist_name = Rofi.run(playlists, "Enter Playlist Name")
|
||||
else:
|
||||
playlist_name = fuzzy_inquirer("Enter Playlist Name: ", playlists)
|
||||
playlist_name = fuzzy_inquirer(
|
||||
playlists,
|
||||
"Enter Playlist Name: ",
|
||||
)
|
||||
if playlist_name == "Exit":
|
||||
exit_app()
|
||||
return
|
||||
|
||||
@@ -63,8 +63,8 @@ def search(config: Config, anime_title: str, episode_range: str):
|
||||
search_result = Rofi.run(choices, "Please Select Title")
|
||||
else:
|
||||
search_result = fuzzy_inquirer(
|
||||
"Please Select Title",
|
||||
choices,
|
||||
"Please Select Title",
|
||||
)
|
||||
|
||||
# ---- fetch selected anime ----
|
||||
@@ -110,7 +110,10 @@ def search(config: Config, anime_title: str, episode_range: str):
|
||||
elif config.use_rofi:
|
||||
episode = Rofi.run(episodes, "Select an episode")
|
||||
else:
|
||||
episode = fuzzy_inquirer("Select episode", episodes)
|
||||
episode = fuzzy_inquirer(
|
||||
episodes,
|
||||
"Select episode",
|
||||
)
|
||||
|
||||
# ---- fetch streams ----
|
||||
with Progress() as progress:
|
||||
@@ -147,7 +150,10 @@ def search(config: Config, anime_title: str, episode_range: str):
|
||||
elif config.use_rofi:
|
||||
server = Rofi.run(servers_names, "Select an link")
|
||||
else:
|
||||
server = fuzzy_inquirer("Select link", servers_names)
|
||||
server = fuzzy_inquirer(
|
||||
servers_names,
|
||||
"Select link",
|
||||
)
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, servers[server]["links"]
|
||||
)
|
||||
|
||||
37
fastanime/cli/commands/update.py
Normal file
37
fastanime/cli/commands/update.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import click
|
||||
|
||||
|
||||
@click.command(help="Helper command to update fastanime to latest")
|
||||
@click.option("--check", "-c", help="Check for the latest release", is_flag=True)
|
||||
def update(
|
||||
check,
|
||||
):
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
|
||||
from ..app_updater import check_for_updates, update_app
|
||||
|
||||
def _print_release(release_data):
|
||||
console = Console()
|
||||
body = Markdown(release_data["body"])
|
||||
tag = github_release_data["tag_name"]
|
||||
tag_title = release_data["name"]
|
||||
github_page_url = release_data["html_url"]
|
||||
console.print(f"Release Page: {github_page_url}")
|
||||
console.print(f"Tag: {tag}")
|
||||
console.print(f"Title: {tag_title}")
|
||||
console.print(body)
|
||||
|
||||
if check:
|
||||
is_update, github_release_data = check_for_updates()
|
||||
if is_update:
|
||||
print(
|
||||
"You are running an older version of fastanime please update to get the latest features"
|
||||
)
|
||||
_print_release(github_release_data)
|
||||
else:
|
||||
print("You are running the latest version of fastanime")
|
||||
_print_release(github_release_data)
|
||||
else:
|
||||
success, github_release_data = update_app()
|
||||
_print_release(github_release_data)
|
||||
@@ -1,39 +1,85 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from rich import print
|
||||
|
||||
from ..constants import USER_CONFIG_PATH, USER_VIDEOS_DIR
|
||||
from ..constants import USER_CONFIG_PATH, USER_DATA_PATH, USER_VIDEOS_DIR
|
||||
from ..libs.rofi import Rofi
|
||||
from ..Utility.user_data_helper import user_data_helper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if TYPE_CHECKING:
|
||||
from ..AnimeProvider import AnimeProvider
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""class that handles and manages configuration and user data throughout the clis lifespan
|
||||
|
||||
Attributes:
|
||||
anime_list: [TODO:attribute]
|
||||
watch_history: [TODO:attribute]
|
||||
fastanime_anilist_app_login_url: [TODO:attribute]
|
||||
anime_provider: [TODO:attribute]
|
||||
user_data: [TODO:attribute]
|
||||
configparser: [TODO:attribute]
|
||||
downloads_dir: [TODO:attribute]
|
||||
provider: [TODO:attribute]
|
||||
use_fzf: [TODO:attribute]
|
||||
use_rofi: [TODO:attribute]
|
||||
skip: [TODO:attribute]
|
||||
icons: [TODO:attribute]
|
||||
preview: [TODO:attribute]
|
||||
translation_type: [TODO:attribute]
|
||||
sort_by: [TODO:attribute]
|
||||
continue_from_history: [TODO:attribute]
|
||||
auto_next: [TODO:attribute]
|
||||
auto_select: [TODO:attribute]
|
||||
use_mpv_mod: [TODO:attribute]
|
||||
quality: [TODO:attribute]
|
||||
notification_duration: [TODO:attribute]
|
||||
error: [TODO:attribute]
|
||||
server: [TODO:attribute]
|
||||
format: [TODO:attribute]
|
||||
force_window: [TODO:attribute]
|
||||
preferred_language: [TODO:attribute]
|
||||
rofi_theme: [TODO:attribute]
|
||||
rofi_theme: [TODO:attribute]
|
||||
rofi_theme_input: [TODO:attribute]
|
||||
rofi_theme_input: [TODO:attribute]
|
||||
rofi_theme_confirm: [TODO:attribute]
|
||||
rofi_theme_confirm: [TODO:attribute]
|
||||
watch_history: [TODO:attribute]
|
||||
anime_list: [TODO:attribute]
|
||||
user: [TODO:attribute]
|
||||
"""
|
||||
|
||||
anime_list: list
|
||||
watch_history: dict
|
||||
fastanime_anilist_app_login_url = (
|
||||
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
|
||||
)
|
||||
anime_provider: "AnimeProvider"
|
||||
user_data = {"watch_history": {}, "animelist": [], "user": {}}
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.initialize_user_data()
|
||||
self.load_config()
|
||||
|
||||
def load_config(self):
|
||||
self.configparser = ConfigParser(
|
||||
{
|
||||
"server": "top",
|
||||
"continue_from_history": "True",
|
||||
"quality": "1080",
|
||||
"auto_next": "False",
|
||||
"auto_select": "True",
|
||||
"sort_by": "search match",
|
||||
"downloads_dir": USER_VIDEOS_DIR,
|
||||
"translation_type": "sub",
|
||||
"server": "top",
|
||||
"continue_from_history": "True",
|
||||
"use_mpv_mod": "false",
|
||||
"force_window": "immediate",
|
||||
"preferred_language": "english",
|
||||
"use_fzf": "False",
|
||||
"preview": "False",
|
||||
@@ -47,8 +93,6 @@ class Config(object):
|
||||
"rofi_theme": "",
|
||||
"rofi_theme_input": "",
|
||||
"rofi_theme_confirm": "",
|
||||
"use_mpv_mod": "false",
|
||||
"force_window": "immediate",
|
||||
}
|
||||
)
|
||||
self.configparser.add_section("stream")
|
||||
@@ -60,7 +104,7 @@ class Config(object):
|
||||
|
||||
self.configparser.read(USER_CONFIG_PATH)
|
||||
|
||||
# --- set defaults ---
|
||||
# --- set config values from file or using defaults ---
|
||||
self.downloads_dir = self.get_downloads_dir()
|
||||
self.provider = self.get_provider()
|
||||
self.use_fzf = self.get_use_fzf()
|
||||
@@ -88,13 +132,14 @@ class Config(object):
|
||||
self.rofi_theme_confirm = self.get_rofi_theme_confirm()
|
||||
Rofi.rofi_theme_confirm = self.rofi_theme_confirm
|
||||
# ---- setup user data ------
|
||||
self.watch_history: dict = user_data_helper.user_data.get("watch_history", {})
|
||||
self.anime_list: list = user_data_helper.user_data.get("animelist", [])
|
||||
self.user: dict = user_data_helper.user_data.get("user", {})
|
||||
self.watch_history: dict = self.user_data.get("watch_history", {})
|
||||
self.anime_list: list = self.user_data.get("animelist", [])
|
||||
self.user: dict = self.user_data.get("user", {})
|
||||
|
||||
def update_user(self, user):
|
||||
self.user = user
|
||||
user_data_helper.update_user_info(user)
|
||||
self.user_data["user"] = user
|
||||
self._update_user_data()
|
||||
|
||||
def update_watch_history(
|
||||
self, anime_id: int, episode: str | None, start_time="0", total_time="0"
|
||||
@@ -108,24 +153,48 @@ class Config(object):
|
||||
}
|
||||
}
|
||||
)
|
||||
user_data_helper.update_watch_history(self.watch_history)
|
||||
self.user_data["watch_history"] = self.watch_history
|
||||
self._update_user_data()
|
||||
|
||||
def update_anime_list(self, anime_id: int, remove=False):
|
||||
if remove:
|
||||
try:
|
||||
self.anime_list.remove(anime_id)
|
||||
print("Succesfully removed :cry:")
|
||||
except Exception:
|
||||
print(anime_id, "Nothing to remove :confused:")
|
||||
else:
|
||||
self.anime_list.append(anime_id)
|
||||
user_data_helper.update_animelist(self.anime_list)
|
||||
print("Succesfully added :smile:")
|
||||
input("Enter to continue...")
|
||||
def initialize_user_data(self):
|
||||
try:
|
||||
if os.path.isfile(USER_DATA_PATH):
|
||||
with open(USER_DATA_PATH, "r") as f:
|
||||
user_data = json.load(f)
|
||||
self.user_data.update(user_data)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def _update_user_data(self):
|
||||
"""method that updates the actual user data file"""
|
||||
with open(USER_DATA_PATH, "w") as f:
|
||||
json.dump(self.user_data, f)
|
||||
|
||||
# getters for user configuration
|
||||
|
||||
# --- general section ---
|
||||
def get_provider(self):
|
||||
return self.configparser.get("general", "provider")
|
||||
|
||||
def get_preferred_language(self):
|
||||
return self.configparser.get("general", "preferred_language")
|
||||
|
||||
def get_downloads_dir(self):
|
||||
return self.configparser.get("general", "downloads_dir")
|
||||
|
||||
def get_icons(self):
|
||||
return self.configparser.getboolean("general", "icons")
|
||||
|
||||
def get_preview(self):
|
||||
return self.configparser.getboolean("general", "preview")
|
||||
|
||||
def get_use_fzf(self):
|
||||
return self.configparser.getboolean("general", "use_fzf")
|
||||
|
||||
# rofi conifiguration
|
||||
def get_use_rofi(self):
|
||||
return self.configparser.getboolean("general", "use_rofi")
|
||||
|
||||
def get_rofi_theme(self):
|
||||
return self.configparser.get("general", "rofi_theme")
|
||||
|
||||
@@ -135,47 +204,18 @@ class Config(object):
|
||||
def get_rofi_theme_confirm(self):
|
||||
return self.configparser.get("general", "rofi_theme_confirm")
|
||||
|
||||
def get_downloads_dir(self):
|
||||
return self.configparser.get("general", "downloads_dir")
|
||||
|
||||
def get_use_fzf(self):
|
||||
return self.configparser.getboolean("general", "use_fzf")
|
||||
|
||||
def get_use_rofi(self):
|
||||
return self.configparser.getboolean("general", "use_rofi")
|
||||
|
||||
# --- stream section ---
|
||||
def get_skip(self):
|
||||
return self.configparser.getboolean("stream", "skip")
|
||||
|
||||
def get_force_window(self):
|
||||
return self.configparser.get("stream", "force_window")
|
||||
|
||||
def get_icons(self):
|
||||
return self.configparser.getboolean("general", "icons")
|
||||
|
||||
def get_preview(self):
|
||||
return self.configparser.getboolean("general", "preview")
|
||||
|
||||
def get_preferred_language(self):
|
||||
return self.configparser.get("general", "preferred_language")
|
||||
|
||||
def get_sort_by(self):
|
||||
return self.configparser.get("anilist", "sort_by")
|
||||
|
||||
def get_continue_from_history(self):
|
||||
return self.configparser.getboolean("stream", "continue_from_history")
|
||||
|
||||
def get_translation_type(self):
|
||||
return self.configparser.get("stream", "translation_type")
|
||||
|
||||
def get_auto_next(self):
|
||||
return self.configparser.getboolean("stream", "auto_next")
|
||||
|
||||
def get_auto_select(self):
|
||||
return self.configparser.getboolean("stream", "auto_select")
|
||||
|
||||
def get_quality(self):
|
||||
return self.configparser.get("stream", "quality")
|
||||
def get_continue_from_history(self):
|
||||
return self.configparser.getboolean("stream", "continue_from_history")
|
||||
|
||||
def get_use_mpv_mod(self):
|
||||
return self.configparser.getboolean("stream", "use_mpv_mod")
|
||||
@@ -186,19 +226,117 @@ class Config(object):
|
||||
def get_error(self):
|
||||
return self.configparser.getint("stream", "error")
|
||||
|
||||
def get_force_window(self):
|
||||
return self.configparser.get("stream", "force_window")
|
||||
|
||||
def get_translation_type(self):
|
||||
return self.configparser.get("stream", "translation_type")
|
||||
|
||||
def get_quality(self):
|
||||
return self.configparser.get("stream", "quality")
|
||||
|
||||
def get_server(self):
|
||||
return self.configparser.get("stream", "server")
|
||||
|
||||
def get_format(self):
|
||||
return self.configparser.get("stream", "format")
|
||||
|
||||
def get_sort_by(self):
|
||||
return self.configparser.get("anilist", "sort_by")
|
||||
|
||||
def update_config(self, section: str, key: str, value: str):
|
||||
self.configparser.set(section, key, value)
|
||||
with open(USER_CONFIG_PATH, "w") as config:
|
||||
self.configparser.write(config)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Config(server:{self.get_server()},quality:{self.get_quality()},auto_next:{self.get_auto_next()},continue_from_history:{self.get_continue_from_history()},sort_by:{self.get_sort_by()},downloads_dir:{self.get_downloads_dir()})"
|
||||
current_config_state = f"""
|
||||
[stream]
|
||||
# Auto continue from watch history
|
||||
continue_from_history = {self.continue_from_history}
|
||||
|
||||
# Preferred language for anime (options: dub, sub)
|
||||
translation_type = {self.translation_type}
|
||||
|
||||
# Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
|
||||
server = {self.server}
|
||||
|
||||
# Auto-select next episode
|
||||
auto_next = {self.auto_next}
|
||||
|
||||
# Auto select the anime provider results with fuzzy find.
|
||||
# Note this wont always be correct.But 99% of the time will be.
|
||||
auto_select = {self.auto_select}
|
||||
|
||||
# whether to skip the opening and ending theme songs
|
||||
# NOTE: requires ani-skip to be in path
|
||||
skip = {self.skip}
|
||||
|
||||
# the maximum delta time in minutes after which the episode should be considered as completed
|
||||
# used in the continue from time stamp
|
||||
error = {self.error}
|
||||
|
||||
# whether to use python-mpv
|
||||
# to enable superior control over the player
|
||||
# adding more options to it
|
||||
use_mpv_mod = {self.use_mpv_mod}
|
||||
|
||||
# the format of downloaded anime and trailer
|
||||
# based on yt-dlp format and passed directly to it
|
||||
# learn more by looking it up on their site
|
||||
# only works for downloaded anime if server=gogoanime
|
||||
# since its the only one that offers different formats
|
||||
# the others tend not to
|
||||
format = {self.format}
|
||||
|
||||
[general]
|
||||
|
||||
# can be [allanime,animepahe]
|
||||
provider = {self.provider}
|
||||
|
||||
# Display language (options: english, romaji)
|
||||
preferred_language = {self.preferred_language}
|
||||
|
||||
# Download directory
|
||||
downloads_dir = {self.downloads_dir}
|
||||
|
||||
# whether to show a preview window when using fzf or rofi
|
||||
preview = {self.preview}
|
||||
|
||||
# whether to use fzf as the interface for the anilist command and others.
|
||||
use_fzf = {self.use_fzf}
|
||||
|
||||
# whether to use rofi for the ui
|
||||
use_rofi = {self.use_rofi}
|
||||
|
||||
# rofi theme to use
|
||||
rofi_theme = {self.rofi_theme}
|
||||
rofi_theme_input = {self.rofi_theme_input}
|
||||
rofi_theme_confirm = {self.rofi_theme_confirm}
|
||||
|
||||
# whether to show the icons
|
||||
icons = {self.icons}
|
||||
|
||||
# the duration in minutes a notification will stay in the screen
|
||||
# used by notifier command
|
||||
notification_duration = {self.notification_duration}
|
||||
"""
|
||||
return current_config_state
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
# WARNING: depracated and will probably be removed
|
||||
def update_anime_list(self, anime_id: int, remove=False):
|
||||
if remove:
|
||||
try:
|
||||
self.anime_list.remove(anime_id)
|
||||
print("Succesfully removed :cry:")
|
||||
except Exception:
|
||||
print(anime_id, "Nothing to remove :confused:")
|
||||
else:
|
||||
self.anime_list.append(anime_id)
|
||||
self.user_data["animelist"] = list(set(self.anime_list))
|
||||
self._update_user_data()
|
||||
print("Succesfully added :smile:")
|
||||
input("Enter to continue...")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,15 +7,17 @@ import textwrap
|
||||
from threading import Thread
|
||||
|
||||
import requests
|
||||
from yt_dlp.utils import clean_html
|
||||
|
||||
from ...constants import APP_CACHE_DIR
|
||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
from ...Utility import anilist_data_helper
|
||||
from ...Utility.utils import remove_html_tags
|
||||
from ..utils.utils import get_true_fg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# this script was written by the fzf devs as an example on how to preview images
|
||||
# its only here for convinience
|
||||
fzf_preview = r"""
|
||||
#
|
||||
# The purpose of this script is to demonstrate how to preview a file or an
|
||||
@@ -95,7 +97,16 @@ fzf-preview(){
|
||||
|
||||
|
||||
# ---- aniskip intergration ----
|
||||
def aniskip(mal_id, episode):
|
||||
def aniskip(mal_id: int, episode: str):
|
||||
"""helper function to be used for setting and getting skip data
|
||||
|
||||
Args:
|
||||
mal_id: mal id of the anime
|
||||
episode: episode number
|
||||
|
||||
Returns:
|
||||
mpv chapter options
|
||||
"""
|
||||
ANISKIP = shutil.which("ani-skip")
|
||||
if not ANISKIP:
|
||||
print("Aniskip not found, please install and try again")
|
||||
@@ -111,37 +122,61 @@ def aniskip(mal_id, episode):
|
||||
# ---- prevew stuff ----
|
||||
# import tempfile
|
||||
|
||||
# NOTE: May change this to a temp dir but there were issues so later
|
||||
WORKING_DIR = APP_CACHE_DIR # tempfile.gettempdir()
|
||||
IMAGES_DIR = os.path.join(WORKING_DIR, "images")
|
||||
if not os.path.exists(IMAGES_DIR):
|
||||
os.mkdir(IMAGES_DIR)
|
||||
INFO_DIR = os.path.join(WORKING_DIR, "info")
|
||||
if not os.path.exists(INFO_DIR):
|
||||
os.mkdir(INFO_DIR)
|
||||
|
||||
IMAGES_CACHE_DIR = os.path.join(WORKING_DIR, "images")
|
||||
if not os.path.exists(IMAGES_CACHE_DIR):
|
||||
os.mkdir(IMAGES_CACHE_DIR)
|
||||
ANIME_INFO_CACHE_DIR = os.path.join(WORKING_DIR, "info")
|
||||
if not os.path.exists(ANIME_INFO_CACHE_DIR):
|
||||
os.mkdir(ANIME_INFO_CACHE_DIR)
|
||||
|
||||
|
||||
def save_image_from_url(url: str, file_name: str):
|
||||
"""Helper function that downloads an image to the FastAnime images cache dir given its url and filename
|
||||
|
||||
Args:
|
||||
url: image url to download
|
||||
file_name: filename to use
|
||||
"""
|
||||
image = requests.get(url)
|
||||
with open(f"{IMAGES_DIR}/{file_name}", "wb") as f:
|
||||
with open(f"{IMAGES_CACHE_DIR}/{file_name}", "wb") as f:
|
||||
f.write(image.content)
|
||||
|
||||
|
||||
def save_info_from_str(info: str, file_name: str):
|
||||
with open(f"{INFO_DIR}/{file_name}", "w") as f:
|
||||
"""Helper function that writes text (anime details and info) to a file given its filename
|
||||
|
||||
Args:
|
||||
info: the information anilist has on the anime
|
||||
file_name: the filename to use
|
||||
"""
|
||||
with open(f"{ANIME_INFO_CACHE_DIR}/{file_name}", "w") as f:
|
||||
f.write(info)
|
||||
|
||||
|
||||
def write_search_results(
|
||||
search_results: list[AnilistBaseMediaDataSchema],
|
||||
titles,
|
||||
workers=None,
|
||||
anilist_results: list[AnilistBaseMediaDataSchema],
|
||||
titles: list[str],
|
||||
workers: int | None = None,
|
||||
):
|
||||
H_COLOR = 215, 0, 95
|
||||
S_COLOR = 208, 208, 208
|
||||
S_WIDTH = 45
|
||||
"""A helper function used by and run in a background thread by get_fzf_preview function inorder to get the actual preview data to be displayed by fzf
|
||||
|
||||
Args:
|
||||
anilist_results: the anilist results from an anilist action
|
||||
titles: sanitized anime titles
|
||||
workers:number of threads to use defaults to as many as possible
|
||||
"""
|
||||
# NOTE: Will probably make this a configuraable option
|
||||
HEADER_COLOR = 215, 0, 95
|
||||
SEPARATOR_COLOR = 208, 208, 208
|
||||
SEPARATOR_WIDTH = 45
|
||||
# use concurency to download and write as fast as possible
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
future_to_task = {}
|
||||
for anime, title in zip(search_results, titles):
|
||||
for anime, title in zip(anilist_results, titles):
|
||||
# actual image url
|
||||
image_url = anime["coverImage"]["large"]
|
||||
future_to_task[executor.submit(save_image_from_url, image_url, title)] = (
|
||||
image_url
|
||||
@@ -149,24 +184,24 @@ def write_search_results(
|
||||
|
||||
# handle the text data
|
||||
template = f"""
|
||||
{get_true_fg("-"*S_WIDTH,*S_COLOR,bold=False)}
|
||||
{get_true_fg('Title(jp):',*H_COLOR)} {anime['title']['romaji']}
|
||||
{get_true_fg('Title(eng):',*H_COLOR)} {anime['title']['english']}
|
||||
{get_true_fg('Popularity:',*H_COLOR)} {anime['popularity']}
|
||||
{get_true_fg('Favourites:',*H_COLOR)} {anime['favourites']}
|
||||
{get_true_fg('Status:',*H_COLOR)} {anime['status']}
|
||||
{get_true_fg('Episodes:',*H_COLOR)} {anime['episodes']}
|
||||
{get_true_fg('Genres:',*H_COLOR)} {anilist_data_helper.format_list_data_with_comma(anime['genres'])}
|
||||
{get_true_fg('Next Episode:',*H_COLOR)} {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode'])}
|
||||
{get_true_fg('Start Date:',*H_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['startDate'])}
|
||||
{get_true_fg('End Date:',*H_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['endDate'])}
|
||||
{get_true_fg("-"*S_WIDTH,*S_COLOR,bold=False)}
|
||||
{get_true_fg('Description:',*H_COLOR)}
|
||||
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
||||
{get_true_fg('Title(jp):',*HEADER_COLOR)} {anime['title']['romaji']}
|
||||
{get_true_fg('Title(eng):',*HEADER_COLOR)} {anime['title']['english']}
|
||||
{get_true_fg('Popularity:',*HEADER_COLOR)} {anime['popularity']}
|
||||
{get_true_fg('Favourites:',*HEADER_COLOR)} {anime['favourites']}
|
||||
{get_true_fg('Status:',*HEADER_COLOR)} {anime['status']}
|
||||
{get_true_fg('Episodes:',*HEADER_COLOR)} {anime['episodes']}
|
||||
{get_true_fg('Genres:',*HEADER_COLOR)} {anilist_data_helper.format_list_data_with_comma(anime['genres'])}
|
||||
{get_true_fg('Next Episode:',*HEADER_COLOR)} {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode'])}
|
||||
{get_true_fg('Start Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['startDate'])}
|
||||
{get_true_fg('End Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['endDate'])}
|
||||
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
||||
{get_true_fg('Description:',*HEADER_COLOR)}
|
||||
"""
|
||||
template = textwrap.dedent(template)
|
||||
template = f"""
|
||||
{template}
|
||||
{textwrap.fill(remove_html_tags(
|
||||
{textwrap.fill(clean_html(
|
||||
str(anime['description'])), width=45)}
|
||||
"""
|
||||
future_to_task[executor.submit(save_info_from_str, template, title)] = title
|
||||
@@ -181,11 +216,22 @@ def write_search_results(
|
||||
|
||||
|
||||
# get rofi icons
|
||||
def get_icons(search_results: list[AnilistBaseMediaDataSchema], titles, workers=None):
|
||||
def get_rofi_icons(
|
||||
anilist_results: list[AnilistBaseMediaDataSchema], titles, workers=None
|
||||
):
|
||||
"""A helper function to make sure that the images are downloaded so they can be used as icons
|
||||
|
||||
Args:
|
||||
titles (list[str]): sanitized titles of the anime; NOTE: its important that they are sanitized since they are used as the filenames of the images
|
||||
workers ([TODO:parameter]): Number of threads to use to download the images; defaults to as many as possible
|
||||
anilist_results: the anilist results from an anilist action
|
||||
"""
|
||||
# use concurrency to download the images as fast as possible
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# load the jobs
|
||||
future_to_url = {}
|
||||
for anime, title in zip(search_results, titles):
|
||||
for anime, title in zip(anilist_results, titles):
|
||||
# actual link to download image from
|
||||
image_url = anime["coverImage"]["large"]
|
||||
future_to_url[executor.submit(save_image_from_url, image_url, title)] = (
|
||||
image_url
|
||||
@@ -196,19 +242,32 @@ def get_icons(search_results: list[AnilistBaseMediaDataSchema], titles, workers=
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
future.result()
|
||||
except Exception as exc:
|
||||
logger.error("%r generated an exception: %s" % (url, exc))
|
||||
except Exception as e:
|
||||
logger.error("%r generated an exception: %s" % (url, e))
|
||||
|
||||
|
||||
def get_preview(search_results: list[AnilistBaseMediaDataSchema], titles, wait=False):
|
||||
def get_fzf_preview(
|
||||
anilist_results: list[AnilistBaseMediaDataSchema], titles, wait=False
|
||||
):
|
||||
"""A helper function that constructs data to be used for the fzf preview
|
||||
|
||||
Args:
|
||||
titles (list[str]): The sanitized titles to use, NOTE: its important that they are sanitized since thay will be used as filenames
|
||||
wait (bool): whether to block the ui as we wait for preview defaults to false
|
||||
anilist_results: the anilist results got from an anilist action
|
||||
|
||||
Returns:
|
||||
THe fzf preview script to use
|
||||
"""
|
||||
# ensure images and info exists
|
||||
background_worker = Thread(
|
||||
target=write_search_results, args=(search_results, titles)
|
||||
target=write_search_results, args=(anilist_results, titles)
|
||||
)
|
||||
background_worker.daemon = True
|
||||
background_worker.start()
|
||||
|
||||
os.environ["SHELL"] = shutil.which("bash") or "sh"
|
||||
# the preview script is in bash so making sure fzf doesnt use any other shell lang to process the preview script
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{} ]; then fzf-preview %s/{}
|
||||
@@ -219,12 +278,11 @@ def get_preview(search_results: list[AnilistBaseMediaDataSchema], titles, wait=F
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
IMAGES_DIR,
|
||||
IMAGES_DIR,
|
||||
INFO_DIR,
|
||||
INFO_DIR,
|
||||
IMAGES_CACHE_DIR,
|
||||
IMAGES_CACHE_DIR,
|
||||
ANIME_INFO_CACHE_DIR,
|
||||
ANIME_INFO_CACHE_DIR,
|
||||
)
|
||||
# preview.replace("\n", ";")
|
||||
if wait:
|
||||
background_worker.join()
|
||||
return preview
|
||||
|
||||
@@ -36,13 +36,17 @@ class MpvPlayer(object):
|
||||
ep_no=None,
|
||||
server="top",
|
||||
):
|
||||
anilist_config = self.anilist_config
|
||||
fastanime_runtime_state = self.fastanime_runtime_state
|
||||
config = self.config
|
||||
episode_number: str = anilist_config.episode_number
|
||||
current_episode_number: str = (
|
||||
fastanime_runtime_state.provider_current_episode_number
|
||||
)
|
||||
quality = config.quality
|
||||
episodes: list = sorted(anilist_config.episodes, key=float)
|
||||
anime_id: int = anilist_config.anime_id
|
||||
anime = anilist_config.anime
|
||||
total_episodes: list = sorted(
|
||||
fastanime_runtime_state.provider_available_episodes, key=float
|
||||
)
|
||||
anime_id_anilist: int = fastanime_runtime_state.selected_anime_id_anilist
|
||||
provider_anime = fastanime_runtime_state.provider_anime
|
||||
translation_type = config.translation_type
|
||||
anime_provider = config.anime_provider
|
||||
self.last_stop_time: str = "0"
|
||||
@@ -53,51 +57,59 @@ class MpvPlayer(object):
|
||||
# next or prev
|
||||
if type == "next":
|
||||
self.mpv_player.show_text("Fetching next episode...")
|
||||
next_episode = episodes.index(episode_number) + 1
|
||||
if next_episode >= len(episodes):
|
||||
next_episode = len(episodes) - 1
|
||||
anilist_config.episode_number = episodes[next_episode]
|
||||
episode_number = anilist_config.episode_number
|
||||
config.update_watch_history(anime_id, str(episode_number))
|
||||
next_episode = total_episodes.index(current_episode_number) + 1
|
||||
if next_episode >= len(total_episodes):
|
||||
next_episode = len(total_episodes) - 1
|
||||
fastanime_runtime_state.provider_current_episode_number = total_episodes[
|
||||
next_episode
|
||||
]
|
||||
current_episode_number = (
|
||||
fastanime_runtime_state.provider_current_episode_number
|
||||
)
|
||||
config.update_watch_history(anime_id_anilist, str(current_episode_number))
|
||||
elif type == "reload":
|
||||
if episode_number not in episodes:
|
||||
if current_episode_number not in total_episodes:
|
||||
self.mpv_player.show_text("Episode not available")
|
||||
return
|
||||
self.mpv_player.show_text("Replaying Episode...")
|
||||
elif type == "custom":
|
||||
if not ep_no or ep_no not in episodes:
|
||||
if not ep_no or ep_no not in total_episodes:
|
||||
self.mpv_player.show_text("Episode number not specified or invalid")
|
||||
self.mpv_player.show_text(
|
||||
f"Acceptable episodes are: {episodes}",
|
||||
f"Acceptable episodes are: {total_episodes}",
|
||||
)
|
||||
return
|
||||
|
||||
self.mpv_player.show_text(f"Fetching episode {ep_no}")
|
||||
episode_number = ep_no
|
||||
config.update_watch_history(anime_id, str(ep_no))
|
||||
anilist_config.episode_number = str(ep_no)
|
||||
current_episode_number = ep_no
|
||||
config.update_watch_history(anime_id_anilist, str(ep_no))
|
||||
fastanime_runtime_state.provider_current_episode_number = str(ep_no)
|
||||
else:
|
||||
self.mpv_player.show_text("Fetching previous episode...")
|
||||
prev_episode = episodes.index(episode_number) - 1
|
||||
prev_episode = total_episodes.index(current_episode_number) - 1
|
||||
if prev_episode <= 0:
|
||||
prev_episode = 0
|
||||
anilist_config.episode_number = episodes[prev_episode]
|
||||
episode_number = anilist_config.episode_number
|
||||
config.update_watch_history(anime_id, str(episode_number))
|
||||
fastanime_runtime_state.provider_current_episode_number = total_episodes[
|
||||
prev_episode
|
||||
]
|
||||
current_episode_number = (
|
||||
fastanime_runtime_state.provider_current_episode_number
|
||||
)
|
||||
config.update_watch_history(anime_id_anilist, str(current_episode_number))
|
||||
# update episode progress
|
||||
if config.user and episode_number:
|
||||
if config.user and current_episode_number:
|
||||
AniList.update_anime_list(
|
||||
{
|
||||
"mediaId": anime_id,
|
||||
"progress": episode_number,
|
||||
"mediaId": anime_id_anilist,
|
||||
"progress": current_episode_number,
|
||||
}
|
||||
)
|
||||
# get them juicy streams
|
||||
episode_streams = anime_provider.get_episode_streams(
|
||||
anime,
|
||||
episode_number,
|
||||
provider_anime,
|
||||
current_episode_number,
|
||||
translation_type,
|
||||
anilist_config.selected_anime_anilist,
|
||||
fastanime_runtime_state.selected_anime_anilist,
|
||||
)
|
||||
if not episode_streams:
|
||||
self.mpv_player.show_text("No streams were found")
|
||||
@@ -124,6 +136,7 @@ class MpvPlayer(object):
|
||||
if not stream_link_:
|
||||
self.mpv_player.show_text("Quality not found")
|
||||
return
|
||||
self.mpv_player._set_property("start", "0")
|
||||
stream_link = stream_link_["link"]
|
||||
return stream_link
|
||||
|
||||
@@ -131,12 +144,12 @@ class MpvPlayer(object):
|
||||
self,
|
||||
stream_link,
|
||||
anime_provider: "AnimeProvider",
|
||||
anilist_config,
|
||||
fastanime_runtime_state,
|
||||
config: "Config",
|
||||
title,
|
||||
):
|
||||
self.anime_provider = anime_provider
|
||||
self.anilist_config = anilist_config
|
||||
self.fastanime_runtime_state = fastanime_runtime_state
|
||||
self.config = config
|
||||
self.last_stop_time: str = "0"
|
||||
self.last_total_time: str = "0"
|
||||
@@ -219,13 +232,15 @@ class MpvPlayer(object):
|
||||
def _toggle_translation_type():
|
||||
translation_type = "sub" if config.translation_type == "dub" else "dub"
|
||||
anime = anime_provider.get_anime(
|
||||
anilist_config._anime["id"],
|
||||
anilist_config.selected_anime_anilist,
|
||||
fastanime_runtime_state.provider_anime_search_result["id"],
|
||||
fastanime_runtime_state.selected_anime_anilist,
|
||||
)
|
||||
if not anime:
|
||||
mpv_player.show_text("Failed to update translation type")
|
||||
return
|
||||
anilist_config.episodes = anime["availableEpisodesDetail"][translation_type]
|
||||
fastanime_runtime_state.provider_available_episodes = anime[
|
||||
"availableEpisodesDetail"
|
||||
][translation_type]
|
||||
config.translation_type = translation_type
|
||||
|
||||
if config.translation_type == "dub":
|
||||
@@ -276,7 +291,7 @@ class MpvPlayer(object):
|
||||
return
|
||||
q = ["360", "720", "1080"]
|
||||
quality = quality_raw.decode()
|
||||
links: list = anilist_config.current_stream_links
|
||||
links: list = fastanime_runtime_state.provider_server_episode_streams
|
||||
q = [link["quality"] for link in links]
|
||||
if quality in q:
|
||||
config.quality = quality
|
||||
|
||||
@@ -5,20 +5,23 @@ import requests
|
||||
|
||||
|
||||
def print_img(url: str):
|
||||
executable = shutil.which("chafa")
|
||||
curl = shutil.which("curl")
|
||||
# curl -sL "$1" | chafa /dev/stdin
|
||||
"""helper funtion to print an image given its url
|
||||
|
||||
if executable is None or curl is None:
|
||||
print("chafa or curl not found")
|
||||
return
|
||||
Args:
|
||||
url: [TODO:description]
|
||||
"""
|
||||
if EXECUTABLE := shutil.which("icat"):
|
||||
subprocess.run([EXECUTABLE, url])
|
||||
else:
|
||||
EXECUTABLE = shutil.which("chafa")
|
||||
|
||||
res = requests.get(url)
|
||||
if res.status_code != 200:
|
||||
print("Error fetching image")
|
||||
return
|
||||
img_bytes = res.content
|
||||
if not img_bytes:
|
||||
print("No image found")
|
||||
img_bytes = subprocess.check_output([curl, "-sL", url])
|
||||
subprocess.run([executable, url, "--size=15x15"], input=img_bytes)
|
||||
if EXECUTABLE is None:
|
||||
print("chafanot found")
|
||||
return
|
||||
|
||||
res = requests.get(url)
|
||||
if res.status_code != 200:
|
||||
print("Error fetching image")
|
||||
return
|
||||
img_bytes = res.content
|
||||
subprocess.run([EXECUTABLE, url, "--size=15x15"], input=img_bytes)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class QueryDict(dict):
|
||||
"""dot.notation access to dictionary attributes"""
|
||||
# TODO: add typing
|
||||
class FastAnimeRuntimeState(dict):
|
||||
"""A class that manages fastanime runtime during anilist command runtime"""
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
@@ -13,7 +14,7 @@ class QueryDict(dict):
|
||||
self.__setitem__(attr, value)
|
||||
|
||||
|
||||
def exit_app(*args):
|
||||
def exit_app(exit_code=0, *args):
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
@@ -24,7 +25,8 @@ def exit_app(*args):
|
||||
try:
|
||||
shutil.get_terminal_size()
|
||||
return (
|
||||
sys.stdin.isatty()
|
||||
sys.stdin
|
||||
and sys.stdin.isatty()
|
||||
and sys.stdout.isatty()
|
||||
and os.getenv("TERM") is not None
|
||||
)
|
||||
@@ -44,17 +46,4 @@ def exit_app(*args):
|
||||
from rich import print
|
||||
|
||||
print("Have a good day :smile:", USER_NAME)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def get_formatted_str(string: str, style):
|
||||
from rich.text import Text
|
||||
|
||||
# Create a Text object with desired style
|
||||
text = Text(string, style="bold red")
|
||||
|
||||
# Convert the Text object to an ANSI string
|
||||
ansi_output = text.__rich_console__(None, None) # pyright:ignore
|
||||
|
||||
# Join the ANSI strings to form the final output
|
||||
"".join(segment.text for segment in ansi_output)
|
||||
sys.exit(exit_code)
|
||||
|
||||
@@ -2,9 +2,6 @@ import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from InquirerPy import inquirer
|
||||
from thefuzz import fuzz
|
||||
|
||||
from ...Utility.data import anime_normalizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if TYPE_CHECKING:
|
||||
@@ -23,20 +20,51 @@ GREEN = "\033[38;2;45;24;45;m"
|
||||
|
||||
|
||||
def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]"):
|
||||
"""Helper function used to filter a list of EpisodeStream objects to one that has a corresponding quality
|
||||
|
||||
Args:
|
||||
quality: the quality to use
|
||||
stream_links: a list of EpisodeStream objects
|
||||
|
||||
Returns:
|
||||
an EpisodeStream object or None incase the quality was not found
|
||||
"""
|
||||
for stream_link in stream_links:
|
||||
if stream_link["quality"] == quality:
|
||||
return stream_link
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix="B"):
|
||||
def format_bytes_to_human(num_of_bytes: float, suffix: str = "B"):
|
||||
"""Helper function usedd to format bytes to human
|
||||
|
||||
Args:
|
||||
num_of_bytes: the number of bytes to format
|
||||
suffix: the suffix to use
|
||||
|
||||
Returns:
|
||||
formated bytes
|
||||
"""
|
||||
for unit in ("", "K", "M", "G", "T", "P", "E", "Z"):
|
||||
if abs(num) < 1024.0:
|
||||
return f"{num:3.1f}{unit}{suffix}"
|
||||
num /= 1024.0
|
||||
return f"{num:.1f}Yi{suffix}"
|
||||
if abs(num_of_bytes) < 1024.0:
|
||||
return f"{num_of_bytes:3.1f}{unit}{suffix}"
|
||||
num_of_bytes /= 1024.0
|
||||
return f"{num_of_bytes:.1f}Yi{suffix}"
|
||||
|
||||
|
||||
def get_true_fg(string: str, r: int, g: int, b: int, bold=True) -> str:
|
||||
def get_true_fg(string: str, r: int, g: int, b: int, bold: bool = True) -> str:
|
||||
"""Custom helper function that enables colored text in the terminal
|
||||
|
||||
Args:
|
||||
bold: whether to bolden the text
|
||||
string: string to color
|
||||
r: red
|
||||
g: green
|
||||
b: blue
|
||||
|
||||
Returns:
|
||||
colored string
|
||||
"""
|
||||
# NOTE: Currently only supports terminals that support true color
|
||||
if bold:
|
||||
return f"{BOLD}\033[38;2;{r};{g};{b};m{string}{RESET}"
|
||||
else:
|
||||
@@ -47,7 +75,17 @@ def get_true_bg(string, r: int, g: int, b: int) -> str:
|
||||
return f"\033[48;2;{r};{g};{b};m{string}{RESET}"
|
||||
|
||||
|
||||
def fuzzy_inquirer(prompt: str, choices, **kwargs):
|
||||
def fuzzy_inquirer(choices: list, prompt: str, **kwargs):
|
||||
"""helper function that enables easier interaction with InquirerPy lib
|
||||
|
||||
Args:
|
||||
choices: the choices to prompt
|
||||
prompt: the prompt string to use
|
||||
**kwargs: other options to pass to fuzzy_inquirer
|
||||
|
||||
Returns:
|
||||
a choice
|
||||
"""
|
||||
from click import clear
|
||||
|
||||
clear()
|
||||
@@ -60,29 +98,3 @@ def fuzzy_inquirer(prompt: str, choices, **kwargs):
|
||||
**kwargs,
|
||||
).execute()
|
||||
return action
|
||||
|
||||
|
||||
def anime_title_percentage_match(
|
||||
possible_user_requested_anime_title: str, title: tuple
|
||||
) -> float:
|
||||
"""Returns the percentage match between the possible title and user title
|
||||
|
||||
Args:
|
||||
possible_user_requested_anime_title (str): an Animdl search result title
|
||||
title (str): the anime title the user wants
|
||||
|
||||
Returns:
|
||||
int: the percentage match
|
||||
"""
|
||||
if normalized_anime_title := anime_normalizer.get(
|
||||
possible_user_requested_anime_title
|
||||
):
|
||||
possible_user_requested_anime_title = normalized_anime_title
|
||||
for key, value in locals().items():
|
||||
logger.info(f"{key}: {value}")
|
||||
# compares both the romaji and english names and gets highest Score
|
||||
percentage_ratio = max(
|
||||
fuzz.ratio(title[0].lower(), possible_user_requested_anime_title.lower()),
|
||||
fuzz.ratio(title[1].lower(), possible_user_requested_anime_title.lower()),
|
||||
)
|
||||
return percentage_ratio
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from platform import system
|
||||
|
||||
from platformdirs import PlatformDirs
|
||||
|
||||
from . import APP_NAME, AUTHOR
|
||||
from . import APP_NAME, AUTHOR, __version__
|
||||
|
||||
PLATFORM = system()
|
||||
dirs = PlatformDirs(appname=APP_NAME, appauthor=AUTHOR, ensure_exists=True)
|
||||
|
||||
|
||||
# ---- app deps ----
|
||||
APP_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
@@ -24,19 +22,63 @@ PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
|
||||
|
||||
|
||||
# ----- user configs and data -----
|
||||
APP_DATA_DIR = dirs.user_config_dir
|
||||
if not APP_DATA_DIR:
|
||||
APP_DATA_DIR = dirs.user_data_dir
|
||||
|
||||
S_PLATFORM = sys.platform
|
||||
|
||||
if S_PLATFORM == "win32":
|
||||
# app data
|
||||
app_data_dir_base = os.getenv("LOCALAPPDATA")
|
||||
if not app_data_dir_base:
|
||||
raise RuntimeError("Could not determine app data dir please report to devs")
|
||||
APP_DATA_DIR = os.path.join(app_data_dir_base, AUTHOR, APP_NAME)
|
||||
|
||||
# cache dir
|
||||
APP_CACHE_DIR = os.path.join(APP_DATA_DIR, "cache")
|
||||
|
||||
# videos dir
|
||||
video_dir_base = os.path.expanduser("~/Videos")
|
||||
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
|
||||
|
||||
elif S_PLATFORM == "darwin":
|
||||
# app data
|
||||
app_data_dir_base = os.path.expanduser("~/Library/Application Support")
|
||||
APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME, __version__)
|
||||
|
||||
# cache dir
|
||||
cache_dir_base = os.path.expanduser("~/Library/Caches")
|
||||
APP_CACHE_DIR = os.path.join(cache_dir_base, APP_NAME, __version__)
|
||||
|
||||
# videos dir
|
||||
video_dir_base = os.path.expanduser("~/Movies")
|
||||
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
|
||||
else:
|
||||
# app data
|
||||
app_data_dir_base = os.environ.get("XDG_CONFIG_HOME", "")
|
||||
if not app_data_dir_base.strip():
|
||||
app_data_dir_base = os.path.expanduser("~/.config")
|
||||
APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME)
|
||||
|
||||
# cache dir
|
||||
cache_dir_base = os.environ.get("XDG_CACHE_HOME", "")
|
||||
if not cache_dir_base.strip():
|
||||
cache_dir_base = os.path.expanduser("~/.cache")
|
||||
APP_CACHE_DIR = os.path.join(cache_dir_base, APP_NAME)
|
||||
|
||||
# videos dir
|
||||
video_dir_base = os.environ.get("XDG_VIDEOS_DIR", "")
|
||||
if not video_dir_base.strip():
|
||||
video_dir_base = os.path.expanduser("~/Videos")
|
||||
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
|
||||
|
||||
# ensure paths exist
|
||||
Path(APP_DATA_DIR).mkdir(parents=True, exist_ok=True)
|
||||
Path(APP_CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
||||
Path(USER_VIDEOS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# useful paths
|
||||
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
|
||||
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
|
||||
NOTIFIER_LOG_FILE_PATH = os.path.join(APP_DATA_DIR, "notifier.log")
|
||||
|
||||
# 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", "Anime fun")
|
||||
|
||||
@@ -18,7 +18,6 @@ from .constants import (
|
||||
USER_AGENT,
|
||||
)
|
||||
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
|
||||
from .normalizer import normalize_anime, normalize_search_results
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterator
|
||||
@@ -106,7 +105,23 @@ class AllAnimeAPI(AnimeProvider):
|
||||
}
|
||||
try:
|
||||
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
|
||||
return normalize_search_results(search_results) # pyright:ignore
|
||||
page_info = search_results["shows"]["pageInfo"]
|
||||
results = []
|
||||
for result in search_results["shows"]["edges"]:
|
||||
normalized_result = {
|
||||
"id": result["_id"],
|
||||
"title": result["name"],
|
||||
"type": result["__typename"],
|
||||
"availableEpisodes": result["availableEpisodes"],
|
||||
}
|
||||
results.append(normalized_result)
|
||||
|
||||
normalized_search_results = {
|
||||
"pageInfo": page_info,
|
||||
"results": results,
|
||||
}
|
||||
return normalized_search_results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"FA(AllAnime): {e}")
|
||||
return {}
|
||||
@@ -123,9 +138,19 @@ class AllAnimeAPI(AnimeProvider):
|
||||
variables = {"showId": allanime_show_id}
|
||||
try:
|
||||
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
|
||||
return normalize_anime(anime["show"])
|
||||
id: str = anime["show"]["_id"]
|
||||
title: str = anime["show"]["name"]
|
||||
availableEpisodesDetail = anime["show"]["availableEpisodesDetail"]
|
||||
type = anime.get("__typename")
|
||||
normalized_anime = {
|
||||
"id": id,
|
||||
"title": title,
|
||||
"availableEpisodesDetail": availableEpisodesDetail,
|
||||
"type": type,
|
||||
}
|
||||
return normalized_anime
|
||||
except Exception as e:
|
||||
logger.error(f"FA(AllAnime): {e}")
|
||||
logger.error(f"AllAnime(get_anime): {e}")
|
||||
return None
|
||||
|
||||
def _get_anime_episode(
|
||||
@@ -323,7 +348,9 @@ if __name__ == "__main__":
|
||||
print("Sth went wrong")
|
||||
break
|
||||
episode_streams_ = anime_provider.get_episode_streams(
|
||||
anime_data, episode, translation.strip()
|
||||
anime_data, # pyright: ignore
|
||||
episode,
|
||||
translation.strip(),
|
||||
)
|
||||
if episode_streams_ is None:
|
||||
raise Exception("Episode not found")
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
from ..types import Anime, AnimeEpisodeDetails, SearchResults
|
||||
from .types import AllAnimeEpisode, AllAnimeSearchResults, AllAnimeShow
|
||||
|
||||
# TODO: scrap this module and do the transformations directly from the provider class
|
||||
|
||||
|
||||
def normalize_search_results(search_results: AllAnimeSearchResults) -> SearchResults:
|
||||
page_info = search_results["shows"]["pageInfo"]
|
||||
results = []
|
||||
for result in search_results["shows"]["edges"]:
|
||||
normalized_result = {
|
||||
"id": result["_id"],
|
||||
"title": result["name"],
|
||||
"type": result["__typename"],
|
||||
"availableEpisodes": result["availableEpisodes"],
|
||||
}
|
||||
results.append(normalized_result)
|
||||
|
||||
normalized_search_results: SearchResults = {
|
||||
"pageInfo": page_info, # pyright:ignore
|
||||
"results": results,
|
||||
}
|
||||
|
||||
return normalized_search_results
|
||||
|
||||
|
||||
def normalize_anime(anime: AllAnimeShow) -> Anime:
|
||||
id: str = anime["_id"]
|
||||
title: str = anime["name"]
|
||||
availableEpisodesDetail: AnimeEpisodeDetails = anime[
|
||||
"availableEpisodesDetail"
|
||||
] # pyright:ignore
|
||||
type = anime.get("__typename")
|
||||
normalized_anime: Anime = { # pyright:ignore
|
||||
"id": id,
|
||||
"title": title,
|
||||
"availableEpisodesDetail": availableEpisodesDetail,
|
||||
"type": type,
|
||||
}
|
||||
return normalized_anime
|
||||
|
||||
|
||||
def normalize_episode(episode: AllAnimeEpisode):
|
||||
pass
|
||||
@@ -121,9 +121,10 @@ class AnimePaheApi(AnimeProvider):
|
||||
for episode in self.anime["data"]
|
||||
if float(episode["episode"]) == float(episode_number)
|
||||
]
|
||||
|
||||
if not episode:
|
||||
logger.error(f"AnimePahe(streams): episode {episode_number} doesn't exist")
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
episode = episode[0]
|
||||
|
||||
anime_id = anime["id"]
|
||||
@@ -157,7 +158,7 @@ class AnimePaheApi(AnimeProvider):
|
||||
logger.warn(
|
||||
"AnimePahe: embed url not found please report to the developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
# get embed page
|
||||
embed_response = self.session.get(embed_url, headers=SERVER_HEADERS)
|
||||
embed = embed_response.text
|
||||
@@ -174,14 +175,14 @@ class AnimePaheApi(AnimeProvider):
|
||||
logger.warn(
|
||||
"AnimePahe: Encoded js not found please report to the developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
# execute the encoded js with node for now or maybe forever in odrder to get a more workable info
|
||||
NODE = shutil.which("node")
|
||||
if not NODE:
|
||||
logger.warn(
|
||||
"AnimePahe: animepahe currently requires node js to extract them juicy streams"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
result = subprocess.run(
|
||||
[NODE, "-e", encoded_js],
|
||||
text=True,
|
||||
@@ -193,14 +194,14 @@ class AnimePaheApi(AnimeProvider):
|
||||
logger.warn(
|
||||
"AnimePahe: could not decode encoded js using node please report to developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
# get that juicy stream
|
||||
match = JUICY_STREAM_REGEX.search(evaluted_js)
|
||||
if not match:
|
||||
logger.warn(
|
||||
"AnimePahe: could not find the juicy stream please report to developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
# get the actual hls stream link
|
||||
juicy_stream = match.group(1)
|
||||
# add the link
|
||||
|
||||
@@ -67,5 +67,5 @@ class EpisodeStream(TypedDict):
|
||||
|
||||
class Server(TypedDict):
|
||||
server: str
|
||||
episode_title: str | None
|
||||
episode_title: str
|
||||
links: list[EpisodeStream]
|
||||
|
||||
59
poetry.lock
generated
59
poetry.lock
generated
@@ -861,20 +861,6 @@ nodeenv = ">=1.6.0"
|
||||
all = ["twine (>=3.4.1)"]
|
||||
dev = ["twine (>=3.4.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyshortcuts"
|
||||
version = "1.9.0"
|
||||
description = "Create desktop and Start Menu shortcuts for python scripts"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pyshortcuts-1.9.0-py3-none-any.whl", hash = "sha256:54d12ed8cd29bf83ac15153ce882a77072f2032b5f979474c519a2bac5af849d"},
|
||||
{file = "pyshortcuts-1.9.0.tar.gz", hash = "sha256:016e89111337f74ce1ba3f4b79b295a643bc70b3e63ce4600247aa4bafa06877"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.2"
|
||||
@@ -897,43 +883,6 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.1"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
|
||||
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "306"
|
||||
description = "Python for Window Extensions"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"},
|
||||
{file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"},
|
||||
{file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"},
|
||||
{file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"},
|
||||
{file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"},
|
||||
{file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"},
|
||||
{file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"},
|
||||
{file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"},
|
||||
{file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"},
|
||||
{file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"},
|
||||
{file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"},
|
||||
{file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"},
|
||||
{file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"},
|
||||
{file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
@@ -1208,13 +1157,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "tox"
|
||||
version = "4.17.0"
|
||||
version = "4.17.1"
|
||||
description = "tox is a generic virtualenv management and test command line tool"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tox-4.17.0-py3-none-any.whl", hash = "sha256:82ef41e7e54182e2143daf0b2920d9030c2e1c4291e12091ebad66860c7be7a4"},
|
||||
{file = "tox-4.17.0.tar.gz", hash = "sha256:b1e2e1dfbfdc174d9be95ae78ec2c4d2cf4800d4c15571deddb197a2c90d2de6"},
|
||||
{file = "tox-4.17.1-py3-none-any.whl", hash = "sha256:2974597c0353577126ab014f52d1a399fb761049e165ff34427f84e8cfe6c990"},
|
||||
{file = "tox-4.17.1.tar.gz", hash = "sha256:2c41565a571e34480bd401d668a4899806169a4633e972ac296c54406d2ded8a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1407,4 +1356,4 @@ test = ["pytest (>=8.1,<9.0)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "83ec7de7d9466dcd1fadef4b21eec2a879cc9a7d526992ed280b6af53b49d9f1"
|
||||
content-hash = "7d20e2d0c0c3c8f3a48d9160a2b4a11a5f353d23bb5d7a06ec527fe08e425b91"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "fastanime"
|
||||
version = "0.62.0.dev1"
|
||||
version = "1.1.0.dev1"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||
license = "UNLICENSE"
|
||||
@@ -12,12 +12,9 @@ yt-dlp = "^2024.5.27"
|
||||
rich = "^13.7.1"
|
||||
click = "^8.1.7"
|
||||
inquirerpy = "^0.3.4"
|
||||
platformdirs = "^4.2.2"
|
||||
python-dotenv = "^1.0.1"
|
||||
thefuzz = "^0.22.1"
|
||||
requests = "^2.32.3"
|
||||
plyer = "^2.1.0"
|
||||
pyshortcuts = "^1.9.0"
|
||||
|
||||
mpv = "^1.0.7"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -55,6 +55,11 @@ def test_completions_help(runner: CliRunner):
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_update_help(runner: CliRunner):
|
||||
result = runner.invoke(run_cli, ["update", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_anilist_help(runner: CliRunner):
|
||||
result = runner.invoke(run_cli, ["anilist", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
Reference in New Issue
Block a user