feat: anilist auth cmd

This commit is contained in:
Benexl
2025-07-22 14:39:16 +03:00
parent f716f9687a
commit 60c583d115
8 changed files with 137 additions and 63 deletions
+18 -6
View File
@@ -1,17 +1,29 @@
import click
from ...interactive.session import session
from ...utils.lazyloader import LazyGroup
from . import examples
commands = {
"trending": "trending.trending",
"recent": "recent.recent",
"search": "search.search",
"download": "download.download",
"downloads": "downloads.downloads",
# "trending": "trending.trending",
# "recent": "recent.recent",
# "search": "search.search",
# "download": "download.download",
# "downloads": "downloads.downloads",
"auth": "auth.auth",
}
@click.command(name="anilist")
@click.group(
cls=LazyGroup,
name="anilist",
root="fastanime.cli.commands.anilist.commands",
invoke_without_command=True,
help="A beautiful interface that gives you access to a commplete streaming experience",
short_help="Access all streaming options",
lazy_subcommands=commands,
epilog=examples.main,
)
@click.option(
"--resume", is_flag=True, help="Resume from the last session (Not yet implemented)."
)
+38 -32
View File
@@ -1,51 +1,57 @@
import click
from rich import print
from rich.prompt import Confirm, Prompt
from re import A
from ....auth.manager import AuthManager # Using the manager
import click
from .....core.config.model import AppConfig
from .....core.constants import ANILIST_AUTH
from .....libs.api.factory import create_api_client
from .....libs.selectors.selector import create_selector
from ....services.auth import AuthService
from ....services.feedback import FeedbackService
@click.command(help="Login to your AniList account to enable progress tracking.")
@click.option("--status", "-s", is_flag=True, help="Check current login status.")
@click.option("--logout", "-l", is_flag=True, help="Log out and erase credentials.")
@click.pass_context
def auth(ctx: click.Context, status: bool, logout: bool):
@click.pass_obj
def auth(config: AppConfig, status: bool, logout: bool):
"""Handles user authentication and credential management."""
manager = AuthManager()
auth_service = AuthService("anilist")
feedback = FeedbackService(config.general.icons)
selector = create_selector(config)
feedback.clear_console()
if status:
user_data = manager.load_user_profile()
user_data = auth_service.get_auth()
if user_data:
print(f"[bold green]Logged in as:[/] {user_data.get('name')}")
print(f"User ID: {user_data.get('id')}")
feedback.info(f"Logged in as: {user_data.user_profile}")
else:
print("[bold yellow]Not logged in.[/]")
feedback.error("Not logged in.")
return
if logout:
if Confirm.ask(
"[bold red]Are you sure you want to log out and erase your token?[/]",
default=False,
):
manager.clear_user_profile()
print("You have been logged out.")
if selector.confirm("Are you sure you want to log out and erase your token?"):
auth_service.clear_user_profile()
feedback.info("You have been logged out.")
return
# --- Start Login Flow ---
from ....libs.api.factory import create_api_client
if auth_profile := auth_service.get_auth():
if not selector.confirm(
f"You are already logged in as {auth_profile.user_profile.name}.Would you like to relogin"
):
return
api_client = create_api_client("anilist", config)
# Create a temporary client just for the login process
api_client = create_api_client("anilist", ctx.obj)
click.launch(
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
# TODO: stop the printing of opening browser session to stderr
click.launch(ANILIST_AUTH)
feedback.info("Your browser has been opened to obtain an AniList token.")
feedback.info(
"After authorizing, copy the token from the address bar and paste it below."
)
print("Your browser has been opened to obtain an AniList token.")
print("After authorizing, copy the token from the address bar and paste it below.")
token = Prompt.ask("Enter your AniList Access Token")
if not token.strip():
print("[bold red]Login cancelled.[/]")
token = selector.ask("Enter your AniList Access Token")
if not token:
feedback.error("Login cancelled.")
return
# Use the API client to validate the token and get profile info
@@ -53,7 +59,7 @@ def auth(ctx: click.Context, status: bool, logout: bool):
if profile:
# If successful, use the manager to save the credentials
manager.save_user_profile(profile, token.strip())
print(f"[bold green]Successfully logged in as {profile.name}! ✨[/]")
auth_service.save_user_profile(profile, token)
feedback.info(f"Successfully logged in as {profile.name}! ✨")
else:
print("[bold red]Login failed. The token may be invalid or expired.[/bold red]")
feedback.error("Login failed. The token may be invalid or expired.")
@@ -0,0 +1,47 @@
main = """
\b
\b\bExamples:
# ---- search ----
\b
# get anime with the tag of isekai
fastanime anilist search -T isekai
\b
# get anime of 2024 and sort by popularity
# that has already finished airing or is releasing
# and is not in your anime lists
fastanime anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --status FINISHED --not-on-list
\b
# get anime of 2024 season WINTER
fastanime anilist search -y 2024 --season WINTER
\b
# get anime genre action and tag isekai,magic
fastanime anilist search -g Action -T Isekai -T Magic
\b
# get anime of 2024 thats finished airing
fastanime anilist search -y 2024 -S FINISHED
\b
# get the most favourite anime movies
fastanime anilist search -f MOVIE -s FAVOURITES_DESC
\b
# ---- login ----
\b
# To sign in just run
fastanime anilist login
\b
# To view your login status
fastanime anilist login --status
\b
# To erase login data
fastanime anilist login --erase
\b
# ---- notifier ----
\b
# basic form
fastanime anilist notifier
\b
# with logging to stdout
fastanime --log anilist notifier
\b
# with logging to a file. stored in the same place as your config
fastanime --log-file anilist notifier
"""
+7 -1
View File
@@ -80,7 +80,13 @@ class Session:
media_api = create_api_client(config.general.media_api, config)
if auth_profile := auth.get_auth():
media_api.authenticate(auth_profile.token)
p = media_api.authenticate(auth_profile.token)
if p:
logger.debug(f"Authenticated as {p.name}")
else:
logger.warning(f"Failed to authenticate with {auth_profile.token}")
else:
logger.debug("Not authenticated")
self._context = Context(
config=config,
+3 -1
View File
@@ -14,7 +14,9 @@ GIT_REPO = "github.com"
GIT_PROTOCOL = "https://"
REPO_HOME = f"https://{GIT_REPO}/{AUTHOR}/FastAnime"
DISCORD_INVITE = "https://discord.gg/C4rhMA4mmK"
ANILIST_AUTH = "https://anilist.co/api/v2/oauth/authorize?client_id=20148"
ANILIST_AUTH = (
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
)
try:
APP_DIR = Path(str(resources.files(PROJECT_NAME.lower())))
+1 -1
View File
@@ -67,7 +67,7 @@ class AniListApi(BaseApiClient):
return None
variables = {
"userId": self.user_profile.id,
"status": params.status,
"status": status_map[params.status] if params.status else None,
"page": params.page,
"perPage": self.config.per_page or params.per_page,
}
+22 -22
View File
@@ -50,15 +50,17 @@ status_map = {
def _to_generic_date(date: AnilistDateObject) -> Optional[datetime]:
return (
datetime(
date["year"],
date["month"],
date["day"],
)
if date and date["year"] and date["month"] and date["day"]
else None
)
if not date:
return
year = date["year"]
month = date["month"]
day = date["day"]
if year:
if not month:
month = 1
if not day:
day = 1
return datetime(year, month, day)
def _to_generic_media_title(anilist_title: AnilistMediaTitle) -> MediaTitle:
@@ -152,24 +154,20 @@ def _to_generic_user_status(
score=anilist_list_entry["score"],
repeat=anilist_list_entry["repeat"],
notes=anilist_list_entry["notes"],
start_date=datetime(
anilist_list_entry["startDate"]["year"],
anilist_list_entry["startDate"]["month"],
anilist_list_entry["startDate"]["day"],
),
completed_at=datetime(
anilist_list_entry["completedAt"]["year"],
anilist_list_entry["completedAt"]["month"],
anilist_list_entry["completedAt"]["day"],
),
created_at=anilist_list_entry["createdAt"],
start_date=_to_generic_date(anilist_list_entry.get("startDate")),
completed_at=_to_generic_date(anilist_list_entry.get("completedAt")),
# TODO: should this be a datetime if so what is the raw values type
created_at=str(anilist_list_entry["createdAt"]),
)
else:
if not anilist_media["mediaListEntry"]:
return
return UserListStatus(
id=anilist_media["mediaListEntry"]["id"],
status=anilist_media["mediaListEntry"]["status"], # type: ignore
status=status_map[anilist_media["mediaListEntry"]["status"]] # pyright: ignore
if anilist_media["mediaListEntry"]["status"]
else None,
progress=anilist_media["mediaListEntry"]["progress"],
)
@@ -233,11 +231,13 @@ def to_generic_search_result(
_to_generic_media_item(item, user_media_list_item)
for item, user_media_list_item in zip(raw_media_list, user_media_list)
]
# TODO: further probe this type
page_info = _to_generic_page_info(page_data) # type: ignore
else:
media_items: List[MediaItem] = [
_to_generic_media_item(item) for item in raw_media_list
]
page_info = _to_generic_page_info(page_data["pageInfo"])
page_info = _to_generic_page_info(page_data["pageInfo"])
return MediaSearchResult(page_info=page_info, media=media_items)
@@ -40,6 +40,7 @@ query (
studios {
nodes {
name
favourites
isAnimationStudio
}
}