Compare commits

...

22 Commits

Author SHA1 Message Date
Benex254
2d921b86c4 docs: update readme 2024-08-02 16:47:22 +03:00
Benex254
603179673d chore: bump version to v0.50.0 2024-08-02 16:47:01 +03:00
Benex254
0ea3cc87ee chore: readd type checking 2024-08-02 16:35:25 +03:00
Benex254
ab9f237c19 fix: correct typing issues 2024-08-02 16:34:36 +03:00
Benex254
1dd7c72b4c feat(interface): improve rofi experience 2024-08-02 15:28:37 +03:00
Benex254
f7cef0eb25 feat(interface): improve the speed of image previews through concurrency 2024-08-02 15:26:52 +03:00
Benex254
07900b3bf8 feat(anilist): lower the maximum allowed calls to the anilist api 2024-08-02 15:23:37 +03:00
Benex254
6479012072 feat: user a better default username 2024-08-02 15:21:50 +03:00
Benex254
0808b8dd38 feat(anilist interface): add rofi as tertiary option for the interface 2024-08-01 12:31:43 +03:00
Benex254
3e2a22612d feat(mpv): use the executable path instead of string 2024-08-01 12:31:03 +03:00
Benex254
45ff21b1af feat(anilist interface): make this more nicer 2024-08-01 12:30:06 +03:00
Benex254
affed01840 feat(anilist api): restrict number of notification results to 5 2024-08-01 12:29:32 +03:00
Benex254
71e707400a feat(notifier)?: add notification bell 2024-08-01 12:28:40 +03:00
Benex254
f0133f718c feat: ui improvements 2024-07-31 14:11:58 +03:00
Benex254
0dc5bfc06b feat: add quick opts for t type 2024-07-31 14:11:36 +03:00
Benex254
a95a118e27 feat: switch to player controls on refuse next episode 2024-07-31 11:08:32 +03:00
Benex254
ace11bc63e feat: update script that runs app for you 2024-07-31 10:58:16 +03:00
Benex254
cc5d65eee3 feat(interface): add breaks for auto next feature 2024-07-31 10:57:44 +03:00
Benex254
ba0de50925 feat: add dockerfile 2024-07-31 09:43:43 +03:00
Benex254
d373ba3bf6 fix: wrong names for options 2024-07-31 09:42:13 +03:00
Benex254
36ce504873 feat: dont force progress onto watchlist 2024-07-31 09:41:40 +03:00
Benex254
47420bedc9 feat: change notification message 2024-07-31 09:40:58 +03:00
25 changed files with 672 additions and 135 deletions

10
Dockerfile Normal file
View 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"]

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ query($id:Int){
"""
notification_query = """
query{
Page {
Page(perPage:5){
pageInfo {
total
}

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

View File

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

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