feat: normalize anime titles

This commit is contained in:
Benex254
2024-08-22 17:32:53 +03:00
parent a26193706e
commit 2b0ade093c
10 changed files with 396 additions and 27 deletions

View File

@@ -9,6 +9,3 @@ anime_normalizer = {
"Dungeon ni Deai o Motomeru no wa Machigatte Iru Darouka": "Dungeon ni Deai wo Motomeru no wa Machigatteiru Darou ka",
'Hazurewaku no "Joutai Ijou Skill" de Saikyou ni Natta Ore ga Subete wo Juurin suru made': "Hazure Waku no [Joutai Ijou Skill] de Saikyou ni Natta Ore ga Subete wo Juurin Suru made",
}
anilist_sort_normalizer = {"search match": "SEARCH_MATCH"}

View File

@@ -4,7 +4,6 @@ import click
from .. import __version__
from ..libs.anime_provider import SERVERS_AVAILABLE, anime_sources
from ..Utility.data import anilist_sort_normalizer
from .commands import LazyGroup
commands = {
@@ -116,9 +115,9 @@ signal.signal(signal.SIGINT, handle_exit)
help="Auto select anime title?",
)
@click.option(
"-S",
"--sort-by",
type=click.Choice(anilist_sort_normalizer.keys()), # pyright: ignore
"--normalize-titles/--no-normalize-titles",
type=bool,
help="whether to normalize anime and episode titls given by providers",
)
@click.option("-d", "--downloads-dir", type=click.Path(), help="Downloads location")
@click.option("--fzf", is_flag=True, help="Use fzf for the ui")
@@ -165,7 +164,7 @@ def run_cli(
quality,
auto_next,
auto_select,
sort_by,
normalize_titles,
downloads_dir,
fzf,
default,
@@ -232,6 +231,11 @@ def run_cli(
ctx.obj.continue_from_history = continue_
if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.skip = skip
if (
ctx.get_parameter_source("normalize_titles")
== click.core.ParameterSource.COMMANDLINE
):
ctx.obj.normalize_titles = normalize_titles
if quality:
ctx.obj.quality = quality
@@ -254,8 +258,6 @@ def run_cli(
== click.core.ParameterSource.COMMANDLINE
):
ctx.obj.use_mpv_mod = use_mpv_mod
if sort_by:
ctx.obj.sort_by = sort_by
if downloads_dir:
ctx.obj.downloads_dir = downloads_dir
if translation_type:

View File

@@ -64,7 +64,7 @@ if TYPE_CHECKING:
)
@click.option(
"--prompt/--no-prompt",
help="Dont prompt for anything instead just do the best thing",
help="Whether to prompt for anything instead just do the best thing",
default=True,
)
@click.pass_obj
@@ -99,6 +99,7 @@ def download(
)
anime_provider = AnimeProvider(config.provider)
anilist_anime_info = None
translation_type = config.translation_type
download_dir = config.downloads_dir
@@ -147,16 +148,18 @@ def download(
}
if config.auto_select:
search_result = max(
selected_anime_title = max(
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
)
print("[cyan]Auto selecting:[/] ", search_result)
print("[cyan]Auto selecting:[/] ", selected_anime_title)
else:
choices = list(search_results_.keys())
if config.use_fzf:
search_result = fzf.run(choices, "Please Select title: ", "FastAnime")
selected_anime_title = fzf.run(
choices, "Please Select title: ", "FastAnime"
)
else:
search_result = fuzzy_inquirer(
selected_anime_title = fuzzy_inquirer(
choices,
"Please Select title",
)
@@ -165,7 +168,7 @@ def download(
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
anime: Anime | None = anime_provider.get_anime(
search_results_[search_result]["id"]
search_results_[selected_anime_title]["id"]
)
if not anime:
print("Sth went wring anime no found")
@@ -215,6 +218,11 @@ def download(
else:
episodes_range = sorted(episodes, key=float)
if config.normalize_titles:
from ...libs.common.mini_anilist import get_basic_anime_info_by_title
anilist_anime_info = get_basic_anime_info_by_title(anime["title"])
# lets download em
for episode in episodes_range:
try:
@@ -279,13 +287,26 @@ def download(
subtitles = servers[server_name]["subtitles"]
episode_title = servers[server_name]["episode_title"]
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
if anilist_anime_info:
selected_anime_title = (
anilist_anime_info["title"][config.preferred_language]
or anilist_anime_info["title"]["romaji"]
or anilist_anime_info["title"]["english"]
)
import re
for episode_detail in anilist_anime_info["episodes"]:
if re.match(f"Episode {episode}", episode_detail["title"]):
episode_title = episode_detail["title"]
break
print(f"[purple]Now Downloading:[/] {episode_title}")
subtitles = move_preferred_subtitle_lang_to_top(
subtitles, config.sub_lang
)
downloader._download_file(
link,
search_result,
selected_anime_title,
episode_title,
download_dir,
silent,

View File

@@ -42,6 +42,7 @@ def search(config: Config, anime_titles: str, episode_range: str):
)
anime_provider = AnimeProvider(config.provider)
anilist_anime_info = None
print(f"[green bold]Streaming:[/] {anime_titles}")
for anime_title in anime_titles:
@@ -123,6 +124,11 @@ def search(config: Config, anime_titles: str, episode_range: str):
episodes_range = iter(episodes_range)
if config.normalize_titles:
from ...libs.common.mini_anilist import get_basic_anime_info_by_title
anilist_anime_info = get_basic_anime_info_by_title(anime["title"])
def stream_anime():
clear()
episode = None
@@ -214,8 +220,23 @@ def search(config: Config, anime_titles: str, episode_range: str):
stream_headers = servers[server]["headers"]
subtitles = servers[server]["subtitles"]
episode_title = servers[server]["episode_title"]
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
selected_anime_title = search_result
if anilist_anime_info:
selected_anime_title = (
anilist_anime_info["title"][config.preferred_language]
or anilist_anime_info["title"]["romaji"]
or anilist_anime_info["title"]["english"]
)
import re
for episode_detail in anilist_anime_info["episodes"]:
if re.match(f"Episode {episode}", episode_detail["title"]):
episode_title = episode_detail["title"]
break
print(
f"[purple]Now Playing:[/] {selected_anime_title} Episode {episode}"
)
subtitles = move_preferred_subtitle_lang_to_top(
subtitles, config.sub_lang
)

View File

@@ -97,6 +97,7 @@ class Config(object):
"rofi_theme_confirm": "",
"ffmpegthumnailer_seek_time": "-1",
"sub_lang": "eng",
"normalize_titles": "true",
}
)
self.configparser.add_section("stream")
@@ -121,6 +122,7 @@ class Config(object):
self.sort_by = self.get_sort_by()
self.continue_from_history = self.get_continue_from_history()
self.auto_next = self.get_auto_next()
self.normalize_titles = self.get_normalize_titles()
self.auto_select = self.get_auto_select()
self.use_mpv_mod = self.get_use_mpv_mod()
self.quality = self.get_quality()
@@ -217,6 +219,9 @@ class Config(object):
def get_rofi_theme_confirm(self):
return self.configparser.get("general", "rofi_theme_confirm")
def get_normalize_titles(self):
return self.configparser.getboolean("general", "normalize_titles")
# --- stream section ---
def get_skip(self):
return self.configparser.getboolean("stream", "skip")

View File

@@ -120,12 +120,24 @@ def media_player_controls(
subtitles = move_preferred_subtitle_lang_to_top(
selected_server["subtitles"], config.sub_lang
)
episode_title = selected_server["episode_title"]
if config.normalize_titles:
import re
for episode_detail in fastanime_runtime_state.selected_anime_anilist[
"streamingEpisodes"
]:
if re.match(
f"Episode {current_episode_number}", episode_detail["title"]
):
episode_title = episode_detail["title"]
break
if config.sync_play:
from ..utils.syncplay import SyncPlayer
stop_time, total_time = SyncPlayer(
current_episode_stream_link,
selected_server["episode_title"],
episode_title,
headers=selected_server["headers"],
subtitles=subtitles,
)
@@ -137,7 +149,7 @@ def media_player_controls(
config.anime_provider,
fastanime_runtime_state,
config,
selected_server["episode_title"],
episode_title,
start_time,
headers=selected_server["headers"],
subtitles=subtitles,
@@ -147,7 +159,7 @@ def media_player_controls(
else:
stop_time, total_time = run_mpv(
current_episode_stream_link,
selected_server["episode_title"],
episode_title,
start_time=start_time,
custom_args=custom_args,
headers=selected_server["headers"],
@@ -514,12 +526,23 @@ def provider_anime_episode_servers_menu(
subtitles = move_preferred_subtitle_lang_to_top(
selected_server["subtitles"], config.sub_lang
)
episode_title = selected_server["episode_title"]
if config.normalize_titles:
import re
for episode_detail in fastanime_runtime_state.selected_anime_anilist[
"streamingEpisodes"
]:
if re.match(f"Episode {current_episode_number}", episode_detail["title"]):
episode_title = episode_detail["title"]
break
if config.sync_play:
from ..utils.syncplay import SyncPlayer
stop_time, total_time = SyncPlayer(
current_stream_link,
selected_server["episode_title"],
episode_title,
headers=selected_server["headers"],
subtitles=subtitles,
)
@@ -533,7 +556,7 @@ def provider_anime_episode_servers_menu(
anime_provider,
fastanime_runtime_state,
config,
selected_server["episode_title"],
episode_title,
start_time,
headers=selected_server["headers"],
subtitles=subtitles,
@@ -547,7 +570,7 @@ def provider_anime_episode_servers_menu(
start_time = "0"
stop_time, total_time = run_mpv(
current_stream_link,
selected_server["episode_title"],
episode_title,
start_time=start_time,
custom_args=custom_args,
headers=selected_server["headers"],
@@ -1284,9 +1307,9 @@ def anilist_results_menu(
choices = [*anime_data.keys(), "Back"]
if config.use_fzf:
if config.preview:
from .utils import get_fzf_preview
from .utils import get_fzf_anime_preview
preview = get_fzf_preview(search_results, anime_data.keys())
preview = get_fzf_anime_preview(search_results, anime_data.keys())
selected_anime_title = fzf.run(
choices,
prompt="Select Anime: ",

View File

@@ -134,6 +134,18 @@ class MpvPlayer(object):
)
return
self.current_media_title = selected_server["episode_title"]
if config.normalize_titles:
import re
for episode_detail in fastanime_runtime_state.selected_anime_anilist[
"streamingEpisodes"
]:
if re.match(
f"Episode {current_episode_number}", episode_detail["title"]
):
self.current_media_title = episode_detail["title"]
break
links = selected_server["links"]
stream_link_ = filter_by_quality(quality, links)

View File

@@ -147,6 +147,11 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
id
}
popularity
streamingEpisodes{
title
thumbnail
}
favourites
averageScore
episodes
@@ -289,6 +294,11 @@ query($query:String,%s){
progress
}
popularity
streamingEpisodes{
title
thumbnail
}
favourites
averageScore
episodes
@@ -346,6 +356,11 @@ query($type:MediaType){
id
}
popularity
streamingEpisodes{
title
thumbnail
}
favourites
averageScore
genres
@@ -412,6 +427,15 @@ query($type:MediaType){
progress
}
popularity
streamingEpisodes{
title
thumbnail
}
streamingEpisodes{
title
thumbnail
}
favourites
averageScore
episodes
@@ -472,6 +496,11 @@ query($type:MediaType){
progress
}
popularity
streamingEpisodes{
title
thumbnail
}
episodes
favourites
averageScore
@@ -527,6 +556,11 @@ query($type:MediaType){
}
popularity
streamingEpisodes{
title
thumbnail
}
favourites
averageScore
description
@@ -591,6 +625,11 @@ query($type:MediaType){
progress
}
popularity
streamingEpisodes{
title
thumbnail
}
favourites
averageScore
description
@@ -659,6 +698,11 @@ query($type:MediaType){
genres
averageScore
popularity
streamingEpisodes{
title
thumbnail
}
favourites
tags {
name
@@ -753,6 +797,11 @@ query ($id: Int,$type:MediaType) {
genres
averageScore
popularity
streamingEpisodes{
title
thumbnail
}
favourites
tags {
name
@@ -827,6 +876,11 @@ query ($page: Int,$type:MediaType) {
progress
}
popularity
streamingEpisodes{
title
thumbnail
}
favourites
averageScore
genres
@@ -943,6 +997,11 @@ query($id:Int){
countryOfOrigin
averageScore
popularity
streamingEpisodes{
title
thumbnail
}
favourites
source
hashtag

View File

@@ -136,6 +136,11 @@ class AnilistMediaListProperties(TypedDict):
hiddenFromStatusLists: bool
class StreamingEpisode(TypedDict):
title: str
thumbnail: str
class AnilistBaseMediaDataSchema(TypedDict):
"""
This a convenience class is used to type the received Anilist data to enhance dev experience
@@ -159,6 +164,7 @@ class AnilistBaseMediaDataSchema(TypedDict):
status: str
nextAiringEpisode: AnilistMediaNextAiringEpisode
season: str
streamingEpisodes: list[StreamingEpisode]
seasonYear: int
duration: int
synonyms: list[str]

View File

@@ -0,0 +1,223 @@
import logging
from typing import TYPE_CHECKING
from requests import post
from thefuzz import fuzz
if TYPE_CHECKING:
from ..anilist.types import AnilistDataSchema
logger = logging.getLogger(__name__)
ANILIST_ENDPOINT = "https://graphql.anilist.co"
"""
query($query:String){
Page(perPage:50){
pageInfo{
total
currentPage
hasNextPage
}
media(search:$query,type:ANIME){
id
idMal
title{
romaji
english
}
episodes
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
def search_foranime_with_anilist(anime_title: str):
query = """
query($query:String){
Page(perPage:50){
pageInfo{
total
currentPage
hasNextPage
}
media(search:$query,type:ANIME){
id
idMal
title{
romaji
english
}
episodes
status
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
}
"""
response = post(
ANILIST_ENDPOINT,
json={"query": query, "variables": {"query": anime_title}},
timeout=10,
)
if response.status_code == 200:
anilist_data: "AnilistDataSchema" = response.json()
return {
"pageInfo": anilist_data["data"]["Page"]["pageInfo"],
"results": [
{
"id": anime_result["id"],
"title": anime_result["title"]["romaji"]
or anime_result["title"]["english"],
"type": "anime",
"availableEpisodes": list(
range(
1,
(
anime_result["episodes"]
if not anime_result["status"] == "RELEASING"
and anime_result["episodes"]
else (
anime_result["nextAiringEpisode"]["episode"] - 1
if anime_result["nextAiringEpisode"]
else 0
)
),
)
),
}
for anime_result in anilist_data["data"]["Page"]["media"]
],
}
def get_mal_id_and_anilist_id(anime_title: str) -> "dict[str,int] | None":
"""the abstraction over all none authenticated requests and that returns data of a similar type
Args:
query: the anilist query
variables: the anilist api variables
Returns:
a boolean indicating success and none or an anilist object depending on success
"""
query = """
query($query:String){
Page(perPage:50){
pageInfo{
total
currentPage
hasNextPage
}
media(search:$query,type:ANIME){
id
idMal
title{
romaji
english
}
}
}
}
"""
try:
variables = {"query": anime_title}
response = post(
ANILIST_ENDPOINT,
json={"query": query, "variables": variables},
timeout=10,
)
anilist_data: "AnilistDataSchema" = response.json()
if response.status_code == 200:
anime = max(
anilist_data["data"]["Page"]["media"],
key=lambda anime: max(
(
fuzz.ratio(anime, str(anime["title"]["romaji"])),
fuzz.ratio(anime_title, str(anime["title"]["english"])),
)
),
)
return {"id_anilist": anime["id"], "id_mal": anime["idMal"]}
except Exception as e:
logger.error(f"Something unexpected occured {e}")
def get_basic_anime_info_by_title(anime_title: str):
"""the abstraction over all none authenticated requests and that returns data of a similar type
Args:
query: the anilist query
variables: the anilist api variables
Returns:
a boolean indicating success and none or an anilist object depending on success
"""
query = """
query($query:String){
Page(perPage:50){
pageInfo{
total
}
media(search:$query,type:ANIME){
id
idMal
title{
romaji
english
}
streamingEpisodes{
title
}
}
}
}
"""
from ...Utility.data import anime_normalizer
# normalize the title
anime_title = anime_normalizer.get(anime_title, anime_title)
try:
variables = {"query": anime_title}
response = post(
ANILIST_ENDPOINT,
json={"query": query, "variables": variables},
timeout=10,
)
anilist_data: "AnilistDataSchema" = response.json()
if response.status_code == 200:
anime = max(
anilist_data["data"]["Page"]["media"],
key=lambda anime: max(
(
fuzz.ratio(anime_title, str(anime["title"]["romaji"])),
fuzz.ratio(anime_title, str(anime["title"]["english"])),
)
),
)
return {
"idAilist": anime["id"],
"idMal": anime["idMal"],
"title": {
"english": anime["title"]["english"],
"romaji": anime["title"]["romaji"],
},
"episodes": [
{"title": episode["title"]}
for episode in anime["streamingEpisodes"]
if episode
],
}
except Exception as e:
logger.error(f"Something unexpected occured {e}")