mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-13 00:00:01 -08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d921b86c4 | ||
|
|
603179673d | ||
|
|
0ea3cc87ee | ||
|
|
ab9f237c19 | ||
|
|
1dd7c72b4c | ||
|
|
f7cef0eb25 | ||
|
|
07900b3bf8 | ||
|
|
6479012072 | ||
|
|
0808b8dd38 | ||
|
|
3e2a22612d | ||
|
|
45ff21b1af | ||
|
|
affed01840 | ||
|
|
71e707400a | ||
|
|
f0133f718c | ||
|
|
0dc5bfc06b | ||
|
|
a95a118e27 | ||
|
|
ace11bc63e | ||
|
|
cc5d65eee3 | ||
|
|
ba0de50925 | ||
|
|
d373ba3bf6 | ||
|
|
36ce504873 | ||
|
|
47420bedc9 |
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM ubuntu
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install python3
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install pipx
|
||||
RUN pipx ensurepath
|
||||
COPY . /fastanime
|
||||
WORKDIR /fastanime
|
||||
RUN pipx install .
|
||||
CMD ["bash"]
|
||||
17
README.md
17
README.md
@@ -182,6 +182,10 @@ Available options include:
|
||||
- `--format <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
|
||||
- `--icons/--no-icons` toggle the visibility of the icons
|
||||
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
|
||||
- `--rofi` use rofi for the ui
|
||||
- `--rofi-theme <path>` theme to use with rofi
|
||||
- `--rofi-theme-input <path>` theme to use with rofi input
|
||||
- `--rofi-theme-confirm <path>` theme to use with rofi confirm
|
||||
|
||||
#### The anilist command :fire: :fire: :fire:
|
||||
|
||||
@@ -236,7 +240,7 @@ For fish users for example you can decide to put this in your `~/.config/fish/co
|
||||
```fish
|
||||
if ! ps aux | grep -q '[f]astanime .* notifier'
|
||||
echo initializing fastanime anilist notifier
|
||||
fastanime --log-file anilist notifier>/dev/null &
|
||||
nohup fastanime --log-file anilist notifier>/dev/null &
|
||||
end
|
||||
```
|
||||
|
||||
@@ -346,8 +350,15 @@ format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
|
||||
[general]
|
||||
preferred_language = romaji # Display language (options: english, romaji)
|
||||
downloads_dir = <Default-videos-dir>/FastAnime # Download directory
|
||||
preview=false # whether to show a preview window when using fzf or rofi
|
||||
|
||||
use_fzf=False # whether to use fzf as the interface for the anilist command and others.
|
||||
preview=false # whether to show a preview window when using fzf
|
||||
|
||||
use_rofi=false # whether to use rofi for the ui
|
||||
rofi_theme=<path-to-rofi-theme-file>
|
||||
rofi_theme_input=<path-to-rofi-theme-file>
|
||||
rofi_theme_confirm=<path-to-rofi-theme-file>
|
||||
|
||||
|
||||
# whether to show the icons
|
||||
icons=false
|
||||
@@ -372,7 +383,7 @@ For inquiries, join our [Discord Server](https://discord.gg/4NUTj5Pt).
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/HRjySFjQ">
|
||||
<img src="https://invidget.switchblade.xyz/HRjySFjQ">
|
||||
<img src="https://invidget.switchblade.xyz/HRjySFjQ"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
4
fa
4
fa
@@ -1,2 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
exec "${PYTHON:-python3}" -Werror -Xdev "$(dirname "$(realpath "$0")")/fastanime/__main__.py" "$@"
|
||||
# exec "${PYTHON:-python3}" -Werror -Xdev -m "$(dirname "$(realpath "$0")")/fastanime" "$@"
|
||||
cd "$(dirname "$(realpath "$0")")" || exit 1
|
||||
exec python -m fastanime "$@"
|
||||
|
||||
@@ -19,7 +19,7 @@ if os.environ.get("FA_RICH_TRACEBACK", False):
|
||||
|
||||
|
||||
# initiate constants
|
||||
__version__ = "v0.40.2"
|
||||
__version__ = "v0.50.0"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
|
||||
@@ -110,6 +110,20 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
type=bool,
|
||||
help="Use icons in the interfaces",
|
||||
)
|
||||
@click.option("--dub", help="Set the translation type to dub", is_flag=True)
|
||||
@click.option("--sub", help="Set the translation type to sub", is_flag=True)
|
||||
@click.option("--rofi", help="Use rofi for the ui", is_flag=True)
|
||||
@click.option("--rofi-theme", help="Rofi theme to use", type=click.Path())
|
||||
@click.option(
|
||||
"--rofi-theme-confirm",
|
||||
help="Rofi theme to use for the confirm prompt",
|
||||
type=click.Path(),
|
||||
)
|
||||
@click.option(
|
||||
"--rofi-theme-input",
|
||||
help="Rofi theme to use for the user input prompt",
|
||||
type=click.Path(),
|
||||
)
|
||||
@click.pass_context
|
||||
def run_cli(
|
||||
ctx: click.Context,
|
||||
@@ -129,6 +143,12 @@ def run_cli(
|
||||
preview,
|
||||
no_preview,
|
||||
icons,
|
||||
dub,
|
||||
sub,
|
||||
rofi,
|
||||
rofi_theme,
|
||||
rofi_theme_confirm,
|
||||
rofi_theme_input,
|
||||
):
|
||||
ctx.obj = Config()
|
||||
if provider:
|
||||
@@ -145,12 +165,12 @@ def run_cli(
|
||||
|
||||
if quality:
|
||||
ctx.obj.quality = quality
|
||||
if ctx.get_parameter_source("auto-next") == click.core.ParameterSource.COMMANDLINE:
|
||||
if ctx.get_parameter_source("auto_next") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.auto_next = auto_next
|
||||
if ctx.get_parameter_source("icons") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.icons = icons
|
||||
if (
|
||||
ctx.get_parameter_source("auto_select")
|
||||
ctx.get_parameter_source("--auto_select")
|
||||
== click.core.ParameterSource.COMMANDLINE
|
||||
):
|
||||
ctx.obj.auto_select = auto_select
|
||||
@@ -168,3 +188,24 @@ def run_cli(
|
||||
ctx.obj.preview = True
|
||||
if no_preview:
|
||||
ctx.obj.preview = False
|
||||
if dub:
|
||||
ctx.obj.translation_type = "dub"
|
||||
if sub:
|
||||
ctx.obj.translation_type = "sub"
|
||||
if rofi:
|
||||
ctx.obj.use_fzf = False
|
||||
ctx.obj.use_rofi = True
|
||||
if rofi:
|
||||
from ..libs.rofi import Rofi
|
||||
|
||||
if rofi_theme:
|
||||
ctx.obj.rofi_theme = rofi_theme
|
||||
Rofi.rofi_theme = rofi_theme
|
||||
|
||||
if rofi_theme_input:
|
||||
ctx.obj.rofi_theme_input = rofi_theme_input
|
||||
Rofi.rofi_theme_input = rofi_theme_input
|
||||
|
||||
if rofi_theme_confirm:
|
||||
ctx.obj.rofi_theme_confirm = rofi_theme_confirm
|
||||
Rofi.rofi_theme_confirm = rofi_theme_confirm
|
||||
|
||||
@@ -15,7 +15,7 @@ def completed(config: Config):
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("COMPLETED")
|
||||
if not anime_list:
|
||||
if not anime_list or not anime_list[1]:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
return
|
||||
|
||||
@@ -17,7 +17,7 @@ def dropped(config: Config):
|
||||
anime_list = AniList.get_anime_list("DROPPED")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
|
||||
@@ -8,7 +8,14 @@ import requests
|
||||
from plyer import notification
|
||||
|
||||
from ....anilist import AniList
|
||||
from ....constants import APP_CACHE_DIR, APP_DATA_DIR, APP_NAME, PLATFORM
|
||||
from ....constants import (
|
||||
APP_CACHE_DIR,
|
||||
APP_DATA_DIR,
|
||||
APP_NAME,
|
||||
ICON_PATH,
|
||||
NOTIFICATION_BELL,
|
||||
PLATFORM,
|
||||
)
|
||||
from ..config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,6 +28,7 @@ def notifier(config: Config):
|
||||
notified = os.path.join(APP_DATA_DIR, "last_notification.json")
|
||||
anime_image = os.path.join(APP_CACHE_DIR, "notification_image")
|
||||
notification_duration = config.notification_duration * 60
|
||||
app_icon = ""
|
||||
|
||||
if not config.user:
|
||||
print("Not Authenticated")
|
||||
@@ -49,6 +57,15 @@ def notifier(config: Config):
|
||||
time.sleep(timeout * 60)
|
||||
continue
|
||||
data = result[1]
|
||||
if not data:
|
||||
print(result)
|
||||
logger.warning(
|
||||
"Something went wrong this could mean anilist is down or you have lost internet connection"
|
||||
)
|
||||
logger.info("sleeping...")
|
||||
time.sleep(timeout * 60)
|
||||
continue
|
||||
|
||||
# pyright:ignore
|
||||
notifications = data["data"]["Page"]["notifications"]
|
||||
if not notifications:
|
||||
@@ -56,12 +73,12 @@ def notifier(config: Config):
|
||||
else:
|
||||
for notification_ in notifications:
|
||||
anime_episode = notification_["episode"]
|
||||
title = f"Episode {anime_episode} just aired"
|
||||
anime_title = notification_["media"]["title"][
|
||||
config.preferred_language
|
||||
]
|
||||
title = f"{anime_title} Episode {anime_episode} just aired"
|
||||
# pyright:ignore
|
||||
message = f"{anime_title}\nBe sure to watch so you are not left out of the loop."
|
||||
message = "Be sure to watch so you are not left out of the loop."
|
||||
# message = str(textwrap.wrap(message, width=50))
|
||||
|
||||
id = notification_["media"]["id"]
|
||||
@@ -80,7 +97,9 @@ def notifier(config: Config):
|
||||
if resp.status_code == 200:
|
||||
with open(anime_image, "wb") as f:
|
||||
f.write(resp.content)
|
||||
ICON_PATH = anime_image
|
||||
app_icon = anime_image
|
||||
else:
|
||||
app_icon = ICON_PATH
|
||||
|
||||
past_notifications[f"{id}"] = notification_["episode"]
|
||||
with open(notified, "w") as f:
|
||||
@@ -90,10 +109,14 @@ def notifier(config: Config):
|
||||
title=title,
|
||||
message=message,
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
hints={"image-path": ICON_PATH},
|
||||
app_icon=app_icon,
|
||||
hints={
|
||||
"image-path": app_icon,
|
||||
"sound-file": NOTIFICATION_BELL,
|
||||
},
|
||||
timeout=notification_duration,
|
||||
)
|
||||
# os.system(f"play {NOTIFICATION_BELL}")
|
||||
time.sleep(30)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
@@ -17,7 +17,7 @@ def paused(config: Config):
|
||||
anime_list = AniList.get_anime_list("PAUSED")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
|
||||
@@ -17,7 +17,7 @@ def planning(config: Config):
|
||||
anime_list = AniList.get_anime_list("PLANNING")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
|
||||
@@ -17,7 +17,7 @@ def rewatching(config: Config):
|
||||
anime_list = AniList.get_anime_list("REPEATING")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
|
||||
@@ -17,7 +17,7 @@ def watching(config: Config):
|
||||
anime_list = AniList.get_anime_list("CURRENT")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
|
||||
@@ -5,6 +5,7 @@ from rich import print
|
||||
|
||||
from ..AnimeProvider import AnimeProvider
|
||||
from ..constants import USER_CONFIG_PATH, USER_VIDEOS_DIR
|
||||
from ..libs.rofi import Rofi
|
||||
from ..Utility.user_data_helper import user_data_helper
|
||||
|
||||
|
||||
@@ -38,6 +39,10 @@ class Config(object):
|
||||
"icons": "false",
|
||||
"notification_duration": "2",
|
||||
"skip": "false",
|
||||
"use_rofi": "false",
|
||||
"rofi_theme": "",
|
||||
"rofi_theme_input": "",
|
||||
"rofi_theme_confirm": "",
|
||||
}
|
||||
)
|
||||
self.configparser.add_section("stream")
|
||||
@@ -46,12 +51,14 @@ class Config(object):
|
||||
if not os.path.exists(USER_CONFIG_PATH):
|
||||
with open(USER_CONFIG_PATH, "w") as config:
|
||||
self.configparser.write(config)
|
||||
|
||||
self.configparser.read(USER_CONFIG_PATH)
|
||||
|
||||
# --- set defaults ---
|
||||
self.downloads_dir = self.get_downloads_dir()
|
||||
self.provider = self.get_provider()
|
||||
self.use_fzf = self.get_use_fzf()
|
||||
self.use_rofi = self.get_use_rofi()
|
||||
self.skip = self.get_skip()
|
||||
self.icons = self.get_icons()
|
||||
self.preview = self.get_preview()
|
||||
@@ -66,7 +73,12 @@ class Config(object):
|
||||
self.server = self.get_server()
|
||||
self.format = self.get_format()
|
||||
self.preferred_language = self.get_preferred_language()
|
||||
|
||||
self.rofi_theme = self.get_rofi_theme()
|
||||
Rofi.rofi_theme = self.rofi_theme
|
||||
self.rofi_theme_input = self.get_rofi_theme_input()
|
||||
Rofi.rofi_theme_input = self.rofi_theme_input
|
||||
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", [])
|
||||
@@ -108,12 +120,24 @@ class Config(object):
|
||||
def get_provider(self):
|
||||
return self.configparser.get("general", "provider")
|
||||
|
||||
def get_rofi_theme(self):
|
||||
return self.configparser.get("general", "rofi_theme")
|
||||
|
||||
def get_rofi_theme_input(self):
|
||||
return self.configparser.get("general", "rofi_theme_input")
|
||||
|
||||
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")
|
||||
|
||||
def get_skip(self):
|
||||
return self.configparser.getboolean("stream", "skip")
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ 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
|
||||
from ...libs.rofi import Rofi
|
||||
from ...Utility.data import anime_normalizer
|
||||
from ...Utility.utils import anime_title_percentage_match, sanitize_filename
|
||||
from ..config import Config
|
||||
@@ -94,6 +95,41 @@ def player_controls(config: Config, anilist_config: QueryDict):
|
||||
player_controls(config, anilist_config)
|
||||
|
||||
def _next_episode():
|
||||
# ensures you dont accidentally erase your progress for an in complete episode
|
||||
stop_time = config.watch_history.get(str(anime_id), {}).get("start_time", "0")
|
||||
|
||||
total_time = config.watch_history.get(str(anime_id), {}).get("total_time", "0")
|
||||
|
||||
error = config.error * 60
|
||||
if stop_time == "0" or total_time == "0":
|
||||
dt = 0
|
||||
else:
|
||||
delta = calculate_time_delta(stop_time, total_time)
|
||||
dt = delta.total_seconds()
|
||||
if dt > error:
|
||||
if config.auto_next:
|
||||
if config.use_rofi:
|
||||
if not Rofi.confirm(
|
||||
"Are you sure you wish to continue to the next episode you haven't completed the current episode?"
|
||||
):
|
||||
anilist_options(config, anilist_config)
|
||||
return
|
||||
else:
|
||||
if not Confirm.ask(
|
||||
"Are you sure you wish to continue to the next episode you haven't completed the current episode?",
|
||||
default=False,
|
||||
):
|
||||
anilist_options(config, anilist_config)
|
||||
return
|
||||
elif not config.use_rofi:
|
||||
if not Confirm.ask(
|
||||
"Are you sure you wish to continue to the next episode, your progress for the current episodes will be erased?",
|
||||
default=True,
|
||||
):
|
||||
player_controls(config, anilist_config)
|
||||
return
|
||||
|
||||
# all checks have passed lets go to the next episode
|
||||
next_episode = episodes.index(current_episode) + 1
|
||||
if next_episode >= len(episodes):
|
||||
next_episode = len(episodes) - 1
|
||||
@@ -136,6 +172,8 @@ def player_controls(config: Config, anilist_config: QueryDict):
|
||||
quality = fzf.run(
|
||||
options, prompt="Select Quality:", header="Quality Options"
|
||||
)
|
||||
elif config.use_rofi:
|
||||
quality = Rofi.run(options, "Select Quality")
|
||||
else:
|
||||
quality = fuzzy_inquirer("Select Quality", options)
|
||||
config.quality = options.index(quality) # set quality
|
||||
@@ -148,6 +186,8 @@ def player_controls(config: Config, anilist_config: QueryDict):
|
||||
translation_type = fzf.run(
|
||||
options, prompt="Select Translation Type: ", header="Lang Options"
|
||||
).lower()
|
||||
elif config.use_rofi:
|
||||
translation_type = Rofi.run(options, "Select Translation Type")
|
||||
else:
|
||||
translation_type = fuzzy_inquirer(
|
||||
"Select Translation Type", options
|
||||
@@ -179,12 +219,15 @@ def player_controls(config: Config, anilist_config: QueryDict):
|
||||
}
|
||||
|
||||
if config.auto_next:
|
||||
print("Auto selecting next episode")
|
||||
_next_episode()
|
||||
return
|
||||
if config.use_fzf:
|
||||
action = fzf.run(
|
||||
list(options.keys()), prompt="Select Action:", header="Player Controls"
|
||||
)
|
||||
elif config.use_rofi:
|
||||
action = Rofi.run(list(options.keys()), "Select Action")
|
||||
else:
|
||||
action = fuzzy_inquirer("Select Action", options.keys())
|
||||
options[action]()
|
||||
@@ -209,8 +252,12 @@ def fetch_streams(config: Config, anilist_config: QueryDict):
|
||||
anime, episode_number, translation_type
|
||||
)
|
||||
if not episode_streams:
|
||||
print("Failed to fetch :cry:")
|
||||
input("Enter to retry...")
|
||||
if not config.use_rofi:
|
||||
print("Failed to fetch :cry:")
|
||||
input("Enter to retry...")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
return fetch_streams(config, anilist_config)
|
||||
|
||||
episode_streams = {
|
||||
@@ -232,6 +279,8 @@ def fetch_streams(config: Config, anilist_config: QueryDict):
|
||||
prompt="Select Server: ",
|
||||
header="Servers",
|
||||
)
|
||||
elif config.use_rofi:
|
||||
server = Rofi.run(choices, "Select Server")
|
||||
else:
|
||||
server = fuzzy_inquirer("Select Server", choices)
|
||||
if server == "Back":
|
||||
@@ -267,7 +316,7 @@ def fetch_streams(config: Config, anilist_config: QueryDict):
|
||||
AniList.update_anime_list(
|
||||
{
|
||||
"mediaId": anime_id,
|
||||
"status": "CURRENT",
|
||||
# "status": "CURRENT",
|
||||
"progress": episode_number,
|
||||
}
|
||||
)
|
||||
@@ -341,6 +390,8 @@ def fetch_episode(config: Config, anilist_config: QueryDict):
|
||||
prompt="Select Episode:",
|
||||
header=anime_title,
|
||||
)
|
||||
elif config.use_rofi:
|
||||
episode_number = Rofi.run(choices, "Select Episode")
|
||||
else:
|
||||
episode_number = fuzzy_inquirer("Select Episode", choices)
|
||||
|
||||
@@ -366,11 +417,14 @@ def fetch_anime_episode(config, anilist_config: QueryDict):
|
||||
progress.add_task("Fetching Anime Info...", total=None)
|
||||
anilist_config.anime = anime_provider.get_anime(selected_anime["id"])
|
||||
if not anilist_config.anime:
|
||||
|
||||
print(
|
||||
"Sth went wrong :cry: this could mean the provider is down or your internet"
|
||||
)
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue...")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
fetch_anime_episode(config, anilist_config)
|
||||
return
|
||||
|
||||
@@ -397,7 +451,11 @@ def provide_anime(config: Config, anilist_config: QueryDict):
|
||||
print(
|
||||
"Sth went wrong :cry: while fetching this could mean you have poor internet connection or the provider is down"
|
||||
)
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue...")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
provide_anime(config, anilist_config)
|
||||
return
|
||||
|
||||
@@ -428,6 +486,8 @@ def provide_anime(config: Config, anilist_config: QueryDict):
|
||||
header="Anime Search Results",
|
||||
)
|
||||
|
||||
elif config.use_rofi:
|
||||
anime_title = Rofi.run(choices, "Select Search Result")
|
||||
else:
|
||||
anime_title = fuzzy_inquirer("Select Search Result", choices)
|
||||
if anime_title == "Back":
|
||||
@@ -452,8 +512,12 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
)
|
||||
anilist_options(config, anilist_config)
|
||||
else:
|
||||
print("no trailer available :confused:")
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
print("no trailer available :confused:")
|
||||
input("Enter to continue...")
|
||||
else:
|
||||
if not Rofi.confirm("No trailler found!!Enter to continue"):
|
||||
exit(0)
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _add_to_list(config: Config, anilist_config: QueryDict):
|
||||
@@ -472,6 +536,10 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
"Choose the list you want to add to",
|
||||
"Add your animelist",
|
||||
)
|
||||
elif config.use_rofi:
|
||||
anime_list = Rofi.run(
|
||||
list(anime_lists.keys()), "Choose list you want to add to"
|
||||
)
|
||||
else:
|
||||
anime_list = fuzzy_inquirer(
|
||||
"Choose the list you want to add to", list(anime_lists.keys())
|
||||
@@ -485,16 +553,21 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
print(
|
||||
f"Successfully added {selected_anime_title} to your {anime_list} list :smile:"
|
||||
)
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue...")
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _score_anime(config: Config, anilist_config: QueryDict):
|
||||
score = inquirer.number(
|
||||
message="Enter the score:",
|
||||
min_allowed=0,
|
||||
max_allowed=100,
|
||||
validate=EmptyInputValidator(),
|
||||
).execute()
|
||||
if config.use_rofi:
|
||||
score = Rofi.ask("Enter Score", is_int=True)
|
||||
score = max(100, min(0, score))
|
||||
else:
|
||||
score = inquirer.number(
|
||||
message="Enter the score:",
|
||||
min_allowed=0,
|
||||
max_allowed=100,
|
||||
validate=EmptyInputValidator(),
|
||||
).execute()
|
||||
|
||||
result = AniList.update_anime_list(
|
||||
{"scoreRaw": score, "mediaId": selected_anime["id"]}
|
||||
@@ -503,7 +576,8 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
print("Failed to update", result)
|
||||
else:
|
||||
print(f"Successfully scored {selected_anime_title}; score: {score}")
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue...")
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _remove_from_list(config: Config, anilist_config: QueryDict):
|
||||
@@ -512,7 +586,7 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
default=False,
|
||||
):
|
||||
success, data = AniList.delete_medialist_entry(selected_anime["id"])
|
||||
if not success:
|
||||
if not success or not data:
|
||||
print("Failed to delete", data)
|
||||
elif not data.get("deleted"):
|
||||
print("Failed to delete", data)
|
||||
@@ -520,7 +594,8 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
print("Successfully deleted :cry:", selected_anime_title)
|
||||
else:
|
||||
print(selected_anime_title, ":relieved:")
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue...")
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _change_translation_type(config: Config, anilist_config: QueryDict):
|
||||
@@ -530,6 +605,8 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
translation_type = fzf.run(
|
||||
options, prompt="Select Translation Type:", header="Language Options"
|
||||
)
|
||||
elif config.use_rofi:
|
||||
translation_type = Rofi.run(options, "Select Translation Type")
|
||||
else:
|
||||
translation_type = fuzzy_inquirer("Select translation type", options)
|
||||
|
||||
@@ -591,6 +668,14 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
anilist_options(config, anilist_config)
|
||||
return
|
||||
|
||||
def _toggle_auto_select(config, anilist_config):
|
||||
config.auto_select = not config.auto_select
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _toggle_auto_next(config, anilist_config):
|
||||
config.auto_select = not config.auto_select
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
icons = config.icons
|
||||
options = {
|
||||
f"{'📽️ ' if icons else ''}Stream": provide_anime,
|
||||
@@ -600,6 +685,8 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
f"{'📤 ' if icons else ''}Remove from List": _remove_from_list,
|
||||
f"{'📖 ' if icons else ''}View Info": _view_info,
|
||||
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
|
||||
f"{'🔘 ' if icons else ''}Toggle auto select anime": _toggle_auto_select, # problematic if you choose an anime that doesnt match id
|
||||
f"{'💠 ' if icons else ''}Toggle auto next episode": _toggle_auto_next,
|
||||
f"{'🔙 ' if icons else ''}Back": select_anime,
|
||||
f"{'❌ ' if icons else ''}Exit": exit_app,
|
||||
}
|
||||
@@ -607,6 +694,8 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
action = fzf.run(
|
||||
list(options.keys()), prompt="Select Action:", header="Anime Menu"
|
||||
)
|
||||
elif config.use_rofi:
|
||||
action = Rofi.run(list(options.keys()), "Select Action")
|
||||
else:
|
||||
action = fuzzy_inquirer("Select Action", options.keys())
|
||||
options[action](config, anilist_config)
|
||||
@@ -639,6 +728,26 @@ def select_anime(config: Config, anilist_config: QueryDict):
|
||||
prompt="Select Anime: ",
|
||||
header="Search Results",
|
||||
)
|
||||
elif config.use_rofi:
|
||||
# TODO: Make this faster
|
||||
if config.preview:
|
||||
from .utils import IMAGES_DIR, get_icons
|
||||
|
||||
get_icons(search_results, config)
|
||||
choices = []
|
||||
for anime in search_results:
|
||||
title = sanitize_filename(
|
||||
str(
|
||||
anime["title"][config.preferred_language]
|
||||
or anime["title"]["romaji"]
|
||||
)
|
||||
)
|
||||
icon_path = os.path.join(IMAGES_DIR, title)
|
||||
choices.append(f"{title}\0icon\x1f{icon_path}")
|
||||
choices.append("Back")
|
||||
selected_anime_title = Rofi.run_with_icons(choices, "Select Anime")
|
||||
else:
|
||||
selected_anime_title = Rofi.run(choices, "Select Anime")
|
||||
else:
|
||||
selected_anime_title = fuzzy_inquirer("Select Anime", choices)
|
||||
# "bat %s/{}" % SEARCH_RESULTS_CACHE
|
||||
@@ -658,8 +767,12 @@ def select_anime(config: Config, anilist_config: QueryDict):
|
||||
|
||||
def handle_animelist(anilist_config, config: Config, list_type: str):
|
||||
if not config.user:
|
||||
print("You haven't logged in please run: fastanime anilist login")
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
print("You haven't logged in please run: fastanime anilist login")
|
||||
input("Enter to continue...")
|
||||
else:
|
||||
if not Rofi.confirm("You haven't logged in!!Enter to continue"):
|
||||
exit(1)
|
||||
anilist(config, anilist_config)
|
||||
return
|
||||
match list_type:
|
||||
@@ -680,12 +793,21 @@ def handle_animelist(anilist_config, config: Config, list_type: str):
|
||||
anime_list = AniList.get_anime_list(status)
|
||||
if not anime_list:
|
||||
print("Sth went wrong", anime_list)
|
||||
input("Enter to continue")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
anilist(config, anilist_config)
|
||||
return
|
||||
if not anime_list[0]:
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
print("Sth went wrong", anime_list)
|
||||
input("Enter to continue")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
|
||||
anilist(config, anilist_config)
|
||||
return
|
||||
media = [
|
||||
@@ -698,7 +820,10 @@ def handle_animelist(anilist_config, config: Config, list_type: str):
|
||||
|
||||
def anilist(config: Config, anilist_config: QueryDict):
|
||||
def _anilist_search():
|
||||
search_term = Prompt.ask("[cyan]Search for[/]")
|
||||
if config.use_rofi:
|
||||
search_term = str(Rofi.ask("Search for"))
|
||||
else:
|
||||
search_term = Prompt.ask("[cyan]Search for[/]")
|
||||
|
||||
return AniList.search(query=search_term)
|
||||
|
||||
@@ -720,7 +845,12 @@ def anilist(config: Config, anilist_config: QueryDict):
|
||||
import subprocess
|
||||
|
||||
subprocess.run([os.environ.get("EDITOR", "open"), USER_CONFIG_PATH])
|
||||
config.load_config()
|
||||
if config.use_rofi:
|
||||
config.load_config()
|
||||
config.use_rofi = True
|
||||
config.use_fzf = False
|
||||
else:
|
||||
config.load_config()
|
||||
|
||||
anilist(config, anilist_config)
|
||||
|
||||
@@ -758,12 +888,13 @@ def anilist(config: Config, anilist_config: QueryDict):
|
||||
f"{'❌ ' if icons else ''}Exit": exit_app,
|
||||
}
|
||||
if config.use_fzf:
|
||||
|
||||
action = fzf.run(
|
||||
list(options.keys()),
|
||||
prompt="Select Action: ",
|
||||
header="Anilist Menu",
|
||||
)
|
||||
elif config.use_rofi:
|
||||
action = Rofi.run(list(options.keys()), "Select Action")
|
||||
else:
|
||||
action = fuzzy_inquirer("Select Action", options.keys())
|
||||
anilist_data = options[action]()
|
||||
@@ -773,5 +904,9 @@ def anilist(config: Config, anilist_config: QueryDict):
|
||||
|
||||
else:
|
||||
print(anilist_data[1])
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue...")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
anilist(config, anilist_config)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import concurrent.futures
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -11,6 +13,9 @@ from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
|
||||
from ...Utility import anilist_data_helper
|
||||
from ...Utility.utils import remove_html_tags, sanitize_filename
|
||||
from ..config import Config
|
||||
from ..utils.utils import get_true_fg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
fzf_preview = r"""
|
||||
#
|
||||
@@ -90,9 +95,7 @@ fzf-preview(){
|
||||
"""
|
||||
|
||||
|
||||
SEARCH_RESULTS_CACHE = os.path.join(APP_CACHE_DIR, "search_results")
|
||||
|
||||
|
||||
# ---- aniskip intergration ----
|
||||
def aniskip(mal_id, episode):
|
||||
ANISKIP = shutil.which("ani-skip")
|
||||
if not ANISKIP:
|
||||
@@ -106,47 +109,62 @@ def aniskip(mal_id, episode):
|
||||
return mpv_skip_args.split(" ")
|
||||
|
||||
|
||||
def write_search_results(
|
||||
search_results: list[AnilistBaseMediaDataSchema], config: Config
|
||||
):
|
||||
for anime in search_results:
|
||||
if not os.path.exists(SEARCH_RESULTS_CACHE):
|
||||
os.mkdir(SEARCH_RESULTS_CACHE)
|
||||
anime_title = (
|
||||
anime["title"][config.preferred_language] or anime["title"]["romaji"]
|
||||
)
|
||||
anime_title = sanitize_filename(anime_title)
|
||||
ANIME_CACHE = os.path.join(SEARCH_RESULTS_CACHE, anime_title)
|
||||
if not os.path.exists(ANIME_CACHE):
|
||||
os.mkdir(ANIME_CACHE)
|
||||
with open(
|
||||
f"{ANIME_CACHE}/image",
|
||||
"wb",
|
||||
) as f:
|
||||
try:
|
||||
image = requests.get(anime["coverImage"]["large"], timeout=5)
|
||||
f.write(image.content)
|
||||
except Exception:
|
||||
pass
|
||||
# ---- prevew stuff ----
|
||||
# import tempfile
|
||||
|
||||
with open(f"{ANIME_CACHE}/data", "w") as f:
|
||||
# data = json.dumps(anime, sort_keys=True, indent=2, separators=(',', ': '))
|
||||
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)
|
||||
|
||||
|
||||
def save_image_from_url(url: str, file_name: str):
|
||||
image = requests.get(url)
|
||||
with open(f"{IMAGES_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:
|
||||
f.write(info)
|
||||
|
||||
|
||||
def write_search_results(
|
||||
search_results: list[AnilistBaseMediaDataSchema], config: Config, workers=None
|
||||
):
|
||||
H_COLOR = 215, 0, 95
|
||||
S_COLOR = 208, 208, 208
|
||||
S_WIDTH = 45
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
future_to_task = {}
|
||||
for anime in search_results:
|
||||
anime_title = (
|
||||
anime["title"][config.preferred_language] or anime["title"]["romaji"]
|
||||
)
|
||||
anime_title = sanitize_filename(anime_title)
|
||||
image_url = anime["coverImage"]["large"]
|
||||
future_to_task[
|
||||
executor.submit(save_image_from_url, image_url, anime_title)
|
||||
] = image_url
|
||||
|
||||
# handle the text data
|
||||
template = f"""
|
||||
{"-"*40}
|
||||
Anime Title(jp): {anime['title']['romaji']}
|
||||
Anime Title(eng): {anime['title']['english']}
|
||||
{"-"*40}
|
||||
Popularity: {anime['popularity']}
|
||||
Favourites: {anime['favourites']}
|
||||
Status: {anime['status']}
|
||||
Episodes: {anime['episodes']}
|
||||
Genres: {anilist_data_helper.format_list_data_with_comma(
|
||||
anime['genres'])}
|
||||
Next Episode: {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode'])}
|
||||
Start Date: {anilist_data_helper.format_anilist_date_object(anime['startDate'])}
|
||||
End Date: {anilist_data_helper.format_anilist_date_object(anime['endDate'])}
|
||||
{"-"*40}
|
||||
Description:
|
||||
{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)}
|
||||
"""
|
||||
template = textwrap.dedent(template)
|
||||
template = f"""
|
||||
@@ -154,32 +172,70 @@ def write_search_results(
|
||||
{textwrap.fill(remove_html_tags(
|
||||
str(anime['description'])), width=45)}
|
||||
"""
|
||||
f.write(template)
|
||||
future_to_task[
|
||||
executor.submit(save_info_from_str, template, anime_title)
|
||||
] = anime_title
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_task):
|
||||
task = future_to_task[future]
|
||||
try:
|
||||
future.result()
|
||||
except Exception as exc:
|
||||
logger.error("%r generated an exception: %s" % (task, exc))
|
||||
|
||||
|
||||
def get_preview(search_results: list[AnilistBaseMediaDataSchema], config: Config):
|
||||
# get rofi icons
|
||||
def get_icons(search_results: list[AnilistBaseMediaDataSchema], config, workers=None):
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# load the jobs
|
||||
future_to_url = {}
|
||||
for anime in search_results:
|
||||
anime_title = (
|
||||
anime["title"][config.preferred_language] or anime["title"]["romaji"]
|
||||
)
|
||||
anime_title = sanitize_filename(anime_title)
|
||||
image_url = anime["coverImage"]["large"]
|
||||
future_to_url[
|
||||
executor.submit(save_image_from_url, image_url, anime_title)
|
||||
] = image_url
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
future.result()
|
||||
except Exception as exc:
|
||||
logger.error("%r generated an exception: %s" % (url, exc))
|
||||
|
||||
|
||||
def get_preview(
|
||||
search_results: list[AnilistBaseMediaDataSchema], config: Config, wait=False
|
||||
):
|
||||
# ensure images and info exists
|
||||
background_worker = Thread(
|
||||
target=write_search_results, args=(search_results, config)
|
||||
)
|
||||
background_worker.daemon = True
|
||||
background_worker.start()
|
||||
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
os.environ["SHELL"] = shutil.which("bash") or "sh"
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{}/image ]; then fzf-preview %s/{}/image
|
||||
if [ -s %s/{} ]; then fzf-preview %s/{}
|
||||
else echo Loading...
|
||||
fi
|
||||
if [ -s %s/{}/data ]; then cat %s/{}/data
|
||||
if [ -s %s/{} ]; then cat %s/{}
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
SEARCH_RESULTS_CACHE,
|
||||
SEARCH_RESULTS_CACHE,
|
||||
SEARCH_RESULTS_CACHE,
|
||||
SEARCH_RESULTS_CACHE,
|
||||
IMAGES_DIR,
|
||||
IMAGES_DIR,
|
||||
INFO_DIR,
|
||||
INFO_DIR,
|
||||
)
|
||||
# preview.replace("\n", ";")
|
||||
if wait:
|
||||
background_worker.join()
|
||||
return preview
|
||||
|
||||
@@ -26,9 +26,9 @@ from typing import Optional
|
||||
#
|
||||
|
||||
|
||||
def stream_video(url, mpv_args, custom_args):
|
||||
def stream_video(MPV, url, mpv_args, custom_args):
|
||||
process = subprocess.Popen(
|
||||
["mpv", url, *mpv_args, *custom_args],
|
||||
[MPV, url, *mpv_args, *custom_args],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
@@ -127,7 +127,7 @@ def mpv(
|
||||
mpv_args.append(f"--title={title}")
|
||||
if ytdl_format:
|
||||
mpv_args.append(f"--ytdl-format={ytdl_format}")
|
||||
stop_time, total_time = stream_video(link, mpv_args, custom_args)
|
||||
stop_time, total_time = stream_video(MPV, link, mpv_args, custom_args)
|
||||
return stop_time, total_time
|
||||
|
||||
|
||||
|
||||
@@ -14,13 +14,36 @@ class QueryDict(dict):
|
||||
|
||||
|
||||
def exit_app(*args):
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from rich import print
|
||||
from ...constants import APP_NAME, ICON_PATH, USER_NAME
|
||||
|
||||
from ...constants import USER_NAME
|
||||
def is_running_in_terminal():
|
||||
try:
|
||||
shutil.get_terminal_size()
|
||||
return (
|
||||
sys.stdin.isatty()
|
||||
and sys.stdout.isatty()
|
||||
and os.getenv("TERM") is not None
|
||||
)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
print("Have a good day :smile:", USER_NAME)
|
||||
if not is_running_in_terminal():
|
||||
from plyer import notification
|
||||
|
||||
notification.notify(
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
message=f"Have a good day {USER_NAME}",
|
||||
title="Shutting down",
|
||||
) # pyright:ignore
|
||||
else:
|
||||
from rich import print
|
||||
|
||||
print("Have a good day :smile:", USER_NAME)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,29 @@ from ...Utility.data import anime_normalizer
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Define ANSI escape codes as constants
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
INVISIBLE_CURSOR = "\033[?25l"
|
||||
VISIBLE_CURSOR = "\033[?25h"
|
||||
UNDERLINE = "\033[4m"
|
||||
|
||||
# ESC[38;2;{r};{g};{b}m
|
||||
BG_GREEN = "\033[48;2;120;233;12;m"
|
||||
GREEN = "\033[38;2;45;24;45;m"
|
||||
|
||||
|
||||
def get_true_fg(string: str, r: int, g: int, b: int, bold=True) -> str:
|
||||
if bold:
|
||||
return f"{BOLD}\033[38;2;{r};{g};{b};m{string}{RESET}"
|
||||
else:
|
||||
return f"\033[38;2;{r};{g};{b};m{string}{RESET}"
|
||||
|
||||
|
||||
def get_true_bg(string, r: int, g: int, b: int) -> str:
|
||||
return f"\033[48;2;{r};{g};{b};m{string}{RESET}"
|
||||
|
||||
|
||||
def clear():
|
||||
if PLATFORM == "Windows":
|
||||
os.system("cls")
|
||||
|
||||
@@ -14,6 +14,10 @@ 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")
|
||||
|
||||
# --- notification bell ---
|
||||
NOTIFICATION_BELL = os.path.join(ASSETS_DIR, "tut_turu.mp3")
|
||||
|
||||
# --- icon stuff ---
|
||||
if PLATFORM == "Windows":
|
||||
ICON_PATH = os.path.join(ASSETS_DIR, "logo.ico")
|
||||
else:
|
||||
@@ -35,4 +39,4 @@ APP_CACHE_DIR = dirs.user_cache_dir
|
||||
USER_VIDEOS_DIR = os.path.join(dirs.user_videos_dir, APP_NAME)
|
||||
|
||||
|
||||
USER_NAME = os.environ.get("USERNAME", f"{APP_NAME} user")
|
||||
USER_NAME = os.environ.get("USERNAME", "Anime fun")
|
||||
|
||||
@@ -165,3 +165,50 @@ class AnilistPages(TypedDict):
|
||||
class AnilistDataSchema(TypedDict):
|
||||
data: AnilistPages
|
||||
Error: str
|
||||
|
||||
|
||||
class AnilistNotification(TypedDict):
|
||||
id: int
|
||||
type: str
|
||||
episode: int
|
||||
context: str
|
||||
createdAt: str
|
||||
media: AnilistBaseMediaDataSchema
|
||||
|
||||
|
||||
class AnilistNotificationPage(TypedDict):
|
||||
pageInfo: AnilistPageInfo
|
||||
notifications: list[AnilistNotification]
|
||||
|
||||
|
||||
class AnilistNotificationPages(TypedDict):
|
||||
Page: AnilistNotificationPage
|
||||
|
||||
|
||||
class AnilistNotifications(TypedDict):
|
||||
data: AnilistNotificationPages
|
||||
|
||||
|
||||
class AnilistMediaList(TypedDict):
|
||||
media: AnilistBaseMediaDataSchema
|
||||
status: str
|
||||
progress: int
|
||||
score: int
|
||||
repeat: int
|
||||
notes: str
|
||||
startDate: AnilistDateObject
|
||||
completedAt: AnilistDateObject
|
||||
createdAt: str
|
||||
|
||||
|
||||
class AnilistMediaListPage(TypedDict):
|
||||
pageInfo: AnilistPageInfo
|
||||
mediaList: list[AnilistMediaList]
|
||||
|
||||
|
||||
class AnilistMediaListPages(TypedDict):
|
||||
Page: AnilistMediaListPage
|
||||
|
||||
|
||||
class AnilistMediaLists(TypedDict):
|
||||
data: AnilistMediaListPages
|
||||
|
||||
@@ -7,7 +7,12 @@ from typing import Literal
|
||||
|
||||
import requests
|
||||
|
||||
from .anilist_data_schema import AnilistDataSchema, AnilistUser
|
||||
from .anilist_data_schema import (
|
||||
AnilistDataSchema,
|
||||
AnilistMediaLists,
|
||||
AnilistNotifications,
|
||||
AnilistUser,
|
||||
)
|
||||
from .queries_graphql import (
|
||||
airing_schedule_query,
|
||||
anime_characters_query,
|
||||
@@ -52,7 +57,9 @@ class AniListApi:
|
||||
self.user_id = user_info["id"] # pyright:ignore
|
||||
return user_info
|
||||
|
||||
def get_notification(self):
|
||||
def get_notification(
|
||||
self,
|
||||
) -> tuple[bool, AnilistNotifications] | tuple[bool, None]:
|
||||
return self._make_authenticated_request(notification_query)
|
||||
|
||||
def reset_notification_count(self):
|
||||
@@ -77,19 +84,22 @@ class AniListApi:
|
||||
status: Literal[
|
||||
"CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"
|
||||
],
|
||||
):
|
||||
) -> tuple[bool, AnilistMediaLists] | tuple[bool, None]:
|
||||
variables = {"status": status, "userId": self.user_id}
|
||||
return self._make_authenticated_request(media_list_query, variables)
|
||||
|
||||
def get_medialist_entry(self, mediaId: int):
|
||||
def get_medialist_entry(
|
||||
self, mediaId: int
|
||||
) -> tuple[bool, dict] | tuple[bool, None]:
|
||||
variables = {"mediaId": mediaId}
|
||||
return self._make_authenticated_request(get_medialist_item_query, variables)
|
||||
|
||||
def delete_medialist_entry(self, mediaId: int):
|
||||
result = self.get_medialist_entry(mediaId)
|
||||
if not result[0]:
|
||||
data = result[1]
|
||||
if not result[0] or not data:
|
||||
return result
|
||||
id = result[1]["data"]["MediaList"]["id"]
|
||||
id = data["data"]["MediaList"]["id"]
|
||||
variables = {"id": id}
|
||||
return self._make_authenticated_request(delete_list_entry_query, variables)
|
||||
|
||||
@@ -117,7 +127,7 @@ class AniListApi:
|
||||
|
||||
# ensuring you dont get blocked
|
||||
if (
|
||||
int(response.headers.get("X-RateLimit-Remaining", 0)) < 5
|
||||
int(response.headers.get("X-RateLimit-Remaining", 0)) < 30
|
||||
and not response.status_code == 500
|
||||
):
|
||||
print(
|
||||
@@ -139,26 +149,16 @@ class AniListApi:
|
||||
logger.warning(
|
||||
"Timeout has been exceeded this could mean anilist is down or you have lost internet connection"
|
||||
)
|
||||
return (
|
||||
False,
|
||||
{
|
||||
"Error": "Timeout Exceeded for connection there might be a problem with your internet or anilist is down."
|
||||
},
|
||||
) # type: ignore
|
||||
return (False, None)
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.warning(
|
||||
"ConnectionError this could mean anilist is down or you have lost internet connection"
|
||||
)
|
||||
return (False, None)
|
||||
|
||||
return (
|
||||
False,
|
||||
{
|
||||
"Error": "There might be a problem with your internet or anilist is down."
|
||||
},
|
||||
) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(f"Something unexpected occured {e}")
|
||||
return (False, {"Error": f"{e}"}) # type: ignore
|
||||
return (False, None) # type: ignore
|
||||
|
||||
def get_watchlist(self):
|
||||
variables = {"status": "CURRENT", "userId": self.user_id}
|
||||
@@ -189,7 +189,7 @@ class AniListApi:
|
||||
|
||||
# ensuring you dont get blocked
|
||||
if (
|
||||
int(response.headers.get("X-RateLimit-Remaining", 0)) < 5
|
||||
int(response.headers.get("X-RateLimit-Remaining", 0)) < 30
|
||||
and not response.status_code == 500
|
||||
):
|
||||
print(
|
||||
|
||||
@@ -35,7 +35,7 @@ query($id:Int){
|
||||
"""
|
||||
notification_query = """
|
||||
query{
|
||||
Page {
|
||||
Page(perPage:5){
|
||||
pageInfo {
|
||||
total
|
||||
}
|
||||
|
||||
138
fastanime/libs/rofi/__init__.py
Normal file
138
fastanime/libs/rofi/__init__.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import subprocess
|
||||
from shutil import which
|
||||
from sys import exit
|
||||
|
||||
from plyer import notification
|
||||
|
||||
from fastanime import APP_NAME
|
||||
|
||||
from ...constants import ICON_PATH
|
||||
|
||||
|
||||
class RofiApi:
|
||||
ROFI_EXECUTABLE = which("rofi")
|
||||
|
||||
rofi_theme = ""
|
||||
rofi_theme_confirm = ""
|
||||
rofi_theme_input = ""
|
||||
|
||||
def run_with_icons(self, options: list[str], prompt_text: str) -> str:
|
||||
rofi_input = "\n".join(options)
|
||||
|
||||
if not self.ROFI_EXECUTABLE:
|
||||
raise Exception("Rofi not found")
|
||||
|
||||
args = [self.ROFI_EXECUTABLE]
|
||||
if self.rofi_theme:
|
||||
args.extend(["-no-config", "-theme", self.rofi_theme])
|
||||
args.extend(["-p", prompt_text, "-i", "-show-icons", "-dmenu"])
|
||||
result = subprocess.run(
|
||||
args,
|
||||
input=rofi_input,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
choice = result.stdout.strip()
|
||||
if not choice:
|
||||
notification.notify(
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
message="FastAnime is shutting down",
|
||||
title="No Valid Input Provided",
|
||||
) # pyright:ignore
|
||||
exit(1)
|
||||
|
||||
return choice
|
||||
|
||||
def run(self, options: list[str], prompt_text: str) -> str:
|
||||
rofi_input = "\n".join(options)
|
||||
|
||||
if not self.ROFI_EXECUTABLE:
|
||||
raise Exception("Rofi not found")
|
||||
|
||||
args = [self.ROFI_EXECUTABLE]
|
||||
if self.rofi_theme:
|
||||
args.extend(["-no-config", "-theme", self.rofi_theme])
|
||||
args.extend(["-p", prompt_text, "-i", "-dmenu"])
|
||||
result = subprocess.run(
|
||||
args,
|
||||
input=rofi_input,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
choice = result.stdout.strip()
|
||||
if not choice or choice not in options:
|
||||
notification.notify(
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
message="FastAnime is shutting down",
|
||||
title="No Valid Input Provided",
|
||||
) # pyright:ignore
|
||||
exit(1)
|
||||
|
||||
return choice
|
||||
|
||||
def confirm(self, prompt_text: str) -> bool:
|
||||
rofi_choices = "Yes\nNo"
|
||||
if not self.ROFI_EXECUTABLE:
|
||||
raise Exception("Rofi not found")
|
||||
args = [self.ROFI_EXECUTABLE]
|
||||
if self.rofi_theme_confirm:
|
||||
args.extend(["-no-config", "-theme", self.rofi_theme_confirm])
|
||||
args.extend(["-p", prompt_text, "-i", "", "-no-fixed-num-lines", "-dmenu"])
|
||||
result = subprocess.run(
|
||||
args,
|
||||
input=rofi_choices,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
choice = result.stdout.strip()
|
||||
if not choice:
|
||||
notification.notify(
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
message="FastAnime is shutting down",
|
||||
title="No Valid Input Provided",
|
||||
) # pyright:ignore
|
||||
exit(1)
|
||||
if choice == "Yes":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def ask(
|
||||
self, prompt_text: str, is_int: bool = False, is_float: bool = False
|
||||
) -> str | float | int:
|
||||
if not self.ROFI_EXECUTABLE:
|
||||
raise Exception("Rofi not found")
|
||||
args = [self.ROFI_EXECUTABLE]
|
||||
if self.rofi_theme_input:
|
||||
args.extend(["-no-config", "-theme", self.rofi_theme_input])
|
||||
args.extend(["-p", prompt_text, "-i", "-no-fixed-num-lines", "-dmenu"])
|
||||
result = subprocess.run(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
user_input = result.stdout.strip()
|
||||
if not user_input:
|
||||
notification.notify(
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
message="FastAnime is shutting down",
|
||||
title="No Valid Input Provided",
|
||||
) # pyright:ignore
|
||||
exit(1)
|
||||
if is_float:
|
||||
user_input = float(user_input)
|
||||
elif is_int:
|
||||
user_input = int(user_input)
|
||||
|
||||
return user_input
|
||||
|
||||
|
||||
Rofi = RofiApi()
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "fastanime"
|
||||
version = "0.40.2"
|
||||
version = "0.50.0"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||
license = "UNLICENSE"
|
||||
|
||||
14
tox.ini
14
tox.ini
@@ -17,10 +17,10 @@ skip_install = true
|
||||
deps =
|
||||
black==22.12
|
||||
commands = black {posargs:.}
|
||||
;
|
||||
; [testenv:type]
|
||||
; description = run type checks
|
||||
; deps =
|
||||
; mypy>=0.991
|
||||
; commands =
|
||||
; mypy {posargs:src tests}
|
||||
|
||||
[testenv:type]
|
||||
description = run type checks
|
||||
deps =
|
||||
mypy>=0.991
|
||||
commands =
|
||||
mypy {posargs:src tests}
|
||||
|
||||
Reference in New Issue
Block a user