feat:switch to poetry as build tool and package manager

This commit is contained in:
Benex254
2024-08-05 09:47:00 +03:00
parent 1bbca16e60
commit 7be9db9724
22 changed files with 2011 additions and 54 deletions

11
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,11 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black

5
fa Executable file
View File

@@ -0,0 +1,5 @@
#! /usr/bin/bash
poetry install
clear
poetry run fastanime $*

View File

@@ -1,4 +0,0 @@
from .data import themes_available
from .media_card_loader import media_card_loader
from .show_notification import show_notification
from .utils import write_crash

View File

@@ -1,5 +1,6 @@
import os
import sys
import logging
from rich import print
from rich.traceback import install
@@ -7,6 +8,8 @@ from rich.traceback import install
import plyer
install()
# Create a logger instance
logger = logging.getLogger(__name__)
# TODO:confirm data integrity
@@ -31,10 +34,27 @@ user_data_path = os.path.join(data_folder, "user_data.json")
assets_folder = os.path.join(app_dir, "assets")
def FastAnime(gui=False):
def FastAnime(gui=False, log=False):
if "--gui" in sys.argv:
gui = True
sys.argv.remove("--gui")
if "--log" in sys.argv:
log = True
sys.argv.remove("--log")
if not log:
logger.propagate = False
else:
# Configure logging
from rich.logging import RichHandler
logging.basicConfig(
level=logging.DEBUG, # Set the logging level to DEBUG
format="%(message)s", # Use a simple message format
datefmt="[%X]", # Use a custom date format
handlers=[RichHandler()], # Use RichHandler to format the logs
)
print(f"Hello {os.environ.get("USERNAME")} from the fastanime team")
if gui:
print(__name__)

View File

@@ -10,7 +10,7 @@ if __package__ is None and not getattr(sys, "frozen", False):
if __name__ == "__main__":
in_development = bool(os.environ.get("IN_DEVELOPMENT", False))
in_development = bool(os.environ.get("IN_DEVELOPMENT", True))
from . import FastAnime
if in_development:

View File

@@ -1,6 +1,14 @@
import click
from ....libs.anilist.anilist import AniList
from .utils import get_search_result
from ...interfaces.anime_interface import anime_interface
@click.command()
def search():
print("search")
@click.option("--title", prompt="Enter anime title")
def search(title):
success, search_results = AniList.search(title)
if search_results and success:
result = get_search_result(search_results)
if result:
anime_interface(result)

View File

@@ -1,6 +1,13 @@
import click
from ....libs.anilist.anilist import AniList
from .utils import get_search_result
from ...interfaces.anime_interface import anime_interface
@click.command()
def trending():
print("trending")
success, trending = AniList.get_trending()
if trending and success:
result = get_search_result(trending)
if result:
anime_interface(result)

View File

@@ -0,0 +1,25 @@
from ...utils.fzf import fzf
from ....libs.anilist.anilist_data_schema import (
AnilistDataSchema,
AnilistBaseMediaDataSchema,
)
def get_search_result(
anilist_data: AnilistDataSchema,
) -> AnilistBaseMediaDataSchema | None:
choices = []
data = anilist_data["data"]["Page"]["media"]
for choice in data:
choices.append(choice["title"]["romaji"])
_selected_anime = fzf(choices)
if not _selected_anime:
return None
def _get_result(x):
return x["title"]["romaji"] == _selected_anime
selected_anime = list(filter(_get_result, data))
if not selected_anime:
return None
return selected_anime[0]

View File

@@ -0,0 +1,6 @@
from .stream_interface import stream_interface
from .info_interface import info_interface
from .binge_interface import binge_interface
from .download_interface import download_interface
from .quit import bye
from .watchlist_interface import watchlist_interface

View File

@@ -0,0 +1,25 @@
import rich
from ..utils.fzf import fzf
from . import (
info_interface,
stream_interface,
binge_interface,
download_interface,
watchlist_interface,
bye,
)
options = {
"info": info_interface,
"stream": stream_interface,
"binge": binge_interface,
"download": download_interface,
"watchlist": watchlist_interface,
"quit": bye,
}
def anime_interface(anime):
command = fzf(options.keys())
if command:
options[command](anime, options)

View File

@@ -0,0 +1,5 @@
from ..utils import fzf
def binge_interface(anime, back):
print(anime)

View File

@@ -0,0 +1,5 @@
from ..utils import fzf
def download_interface(anime, back):
print(anime)

View File

@@ -0,0 +1,5 @@
from ..utils import fzf
def info_interface(anime, back):
print(anime)

View File

@@ -0,0 +1,7 @@
import sys
from rich import print
def bye(*args):
print("Goodbye")
sys.exit()

View File

@@ -0,0 +1,103 @@
from ..utils.fzf import fzf
from ...libs.anime_provider.allanime.api import anime_provider
from ...Utility.data import anime_normalizer
from ..utils.mpv import mpv
from fuzzywuzzy import fuzz
import logging
logger = logging.getLogger(__name__)
def back_(anime, options):
command = fzf(options.keys())
if command:
options[command](anime, options)
def anime_title_percentage_match(
possible_user_requested_anime_title: str, title: tuple
) -> float:
"""Returns the percentage match between the possible title and user title
Args:
possible_user_requested_anime_title (str): an Animdl search result title
title (str): the anime title the user wants
Returns:
int: the percentage match
"""
if normalized_anime_title := anime_normalizer.get(
possible_user_requested_anime_title
):
possible_user_requested_anime_title = normalized_anime_title
for key, value in locals().items():
logger.info(f"{key}: {value}")
# compares both the romaji and english names and gets highest Score
percentage_ratio = max(
fuzz.ratio(title[0].lower(), possible_user_requested_anime_title.lower()),
fuzz.ratio(title[1].lower(), possible_user_requested_anime_title.lower()),
)
return percentage_ratio
def get_matched_result(anime_title, _search_results):
result = max(
_search_results,
key=lambda x: anime_title_percentage_match(x, anime_title),
)
return result
def _get_result(result, compare):
return result["name"] == compare
def _get_server(server, server_name):
return server[0] == server_name
def stream_interface(_anime, back, prefered_translation="sub"):
results = anime_provider.search_for_anime(_anime["title"]["romaji"])
if results:
_search_results = [result["name"] for result in results["shows"]["edges"]]
anime_title = get_matched_result(
(_anime["title"]["romaji"], _anime["title"]["english"]), _search_results
)
result = list(
filter(lambda x: _get_result(x, anime_title), results["shows"]["edges"])
)
if not result:
return
anime = anime_provider.get_anime(result[0]["_id"])
episode = fzf(anime["show"]["availableEpisodesDetail"][prefered_translation])
if not episode:
return
if t_type := fzf(["sub", "dub"]):
prefered_translation = t_type
_episode_streams = anime_provider.get_anime_episode(
result[0]["_id"], episode, prefered_translation
)
if _episode_streams:
episode_streams = anime_provider.get_episode_streams(_episode_streams)
if not episode_streams:
return
servers = list(episode_streams)
_sever = fzf([server[0] for server in servers])
if not _sever:
return
server = list(filter(lambda x: _get_server(x, _sever), servers)).pop()
if not server:
return
#
stream_link = server[1]["links"][0]["link"]
mpv(stream_link)
#
# mpv_player.run_mpv(stream_link)
stream_interface(_anime, back, prefered_translation)

View File

@@ -0,0 +1,5 @@
from ..utils import fzf
def watchlist_interface(anime, back):
print(anime)

View File

View File

@@ -1,10 +1,11 @@
import subprocess
import logging
import shutil
logger = logging.getLogger(__name__)
def run_fzf(options: tuple[str], *custom_commands):
def fzf(options, prompt="Select Anime: ", *custom_commands):
"""
Run fzf with a list of options and return the selected option.
"""
@@ -12,8 +13,21 @@ def run_fzf(options: tuple[str], *custom_commands):
options_str = "\n".join(options)
# Run fzf as a subprocess
FZF = shutil.which("fzf")
if not FZF:
logger.error("fzf not found")
return None
result = subprocess.run(
["fzf", *custom_commands],
[
FZF,
"--reverse",
"--cycle",
"--prompt",
prompt,
]
if not custom_commands
else [FZF, *custom_commands],
input=options_str,
text=True,
stdout=subprocess.PIPE,

View File

@@ -0,0 +1,11 @@
import subprocess
import shutil
import sys
def mpv(link, *custom_args):
MPV = shutil.which("mpv")
if not MPV:
return
subprocess.run([MPV, *custom_args, link])
sys.stdout.flush()

View File

@@ -2,7 +2,8 @@ import json
import logging
import requests
from rich.progress import Progress
from rich import print
from .gql_queries import ALLANIME_SHOW_GQL, ALLANIME_SEARCH_GQL, ALLANIME_EPISODES_GQL
from .constants import (
ALLANIME_BASE,
@@ -60,11 +61,18 @@ class AllAnimeAPI:
"translationtype": translationtype,
"countryorigin": countryorigin,
}
return self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
with Progress() as progress:
progress.add_task("[cyan]searching..", start=False, total=None)
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
return search_results
def get_anime(self, allanime_show_id: str):
variables = {"showId": allanime_show_id}
return anime_provider._fetch_gql(ALLANIME_SHOW_GQL, variables)
with Progress() as progress:
progress.add_task("[cyan]fetching anime..", start=False, total=None)
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
return anime
def get_anime_episode(
self, allanime_show_id: str, episode_string: str, translation_type: str = "sub"
@@ -74,7 +82,10 @@ class AllAnimeAPI:
"translationType": translation_type,
"episodeString": episode_string,
}
return anime_provider._fetch_gql(ALLANIME_EPISODES_GQL, variables)
with Progress() as progress:
progress.add_task("[cyan]fetching episode..", start=False, total=None)
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
return episode
def get_episode_streams(self, allanime_episode_embeds_data):
if (
@@ -83,47 +94,59 @@ class AllAnimeAPI:
):
return {}
embeds = allanime_episode_embeds_data["episode"]["sourceUrls"]
for embed in embeds:
# filter the working streams
if embed.get("sourceName", "") not in ("Sak", "Kir", "S-mp4", "Luf-mp4"):
continue
url = embed.get("sourceUrl")
with Progress() as progress:
progress.add_task("[cyan]fetching streams..", start=False, total=None)
for embed in embeds:
# filter the working streams
if embed.get("sourceName", "") not in (
"Sak",
"Kir",
"S-mp4",
"Luf-mp4",
):
continue
url = embed.get("sourceUrl")
if not url:
continue
if url.startswith("--"):
url = url[2:]
if not url:
continue
if url.startswith("--"):
url = url[2:]
# get the stream url for an episode of the defined source names
parsed_url = decode_hex_string(url)
embed_url = (
f"https://{ALLANIME_BASE}{parsed_url.replace('clock','clock.json')}"
)
resp = requests.get(
embed_url,
headers={
"Referer": ALLANIME_REFERER,
"User-Agent": USER_AGENT,
},
)
if resp.status_code == 200:
match embed["sourceName"]:
case "Luf-mp4":
Logger.debug("allanime:Found streams from gogoanime")
yield "gogoanime", resp.json()
case "Kir":
Logger.debug("allanime:Found streams from wetransfer")
yield "wetransfer", resp.json()
case "S-mp4":
Logger.debug("allanime:Found streams from sharepoint")
yield "sharepoint", resp.json()
case "Sak":
Logger.debug("allanime:Found streams from dropbox")
yield "dropbox", resp.json()
case _:
yield "Unknown", resp.json()
else:
return {}
# get the stream url for an episode of the defined source names
parsed_url = decode_hex_string(url)
embed_url = (
f"https://{ALLANIME_BASE}{parsed_url.replace('clock','clock.json')}"
)
resp = requests.get(
embed_url,
headers={
"Referer": ALLANIME_REFERER,
"User-Agent": USER_AGENT,
},
)
if resp.status_code == 200:
match embed["sourceName"]:
case "Luf-mp4":
Logger.debug("allanime:Found streams from gogoanime")
print("[yellow]gogoanime")
yield "gogoanime", resp.json()
case "Kir":
Logger.debug("allanime:Found streams from wetransfer")
print("[yellow]wetransfer")
yield "wetransfer", resp.json()
case "S-mp4":
Logger.debug("allanime:Found streams from sharepoint")
print("[yellow]sharepoint")
yield "sharepoint", resp.json()
case "Sak":
Logger.debug("allanime:Found streams from dropbox")
print("[yellow]dropbox")
yield "dropbox", resp.json()
case _:
yield "Unknown", resp.json()
else:
return {}
anime_provider = AllAnimeAPI()

1643
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

33
pyproject.toml Normal file
View File

@@ -0,0 +1,33 @@
[tool.poetry]
name = "fastanime"
version = "0.3.0"
description = "A fast and efficient GUI and CLI anime scrapper"
authors = ["Benex254 <benedictx855@gmail.com>"]
license = "UNLICENSE"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
kivy = "^2.3.0"
yt-dlp = "^2024.5.27"
ffpyplayer = "^4.5.1"
plyer = "^2.1.0"
fuzzywuzzy = "^0.18.0"
rich = "^13.7.1"
click = "^8.1.7"
python-levenshtein = "^0.25.1"
kivymd = {url = "https://github.com/kivymd/KivyMD/archive/master.zip"}
[tool.poetry.group.dev.dependencies]
black = "^24.4.2"
isort = "^5.13.2"
pytest = "^8.2.2"
ruff = "^0.4.10"
pre-commit = "^3.7.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
fastanime = 'fastanime:FastAnime'