mirror of
https://github.com/Benexl/FastAnime.git
synced 2026-07-11 22:51:38 -07:00
feat: anilist auth cmd
This commit is contained in:
@@ -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)."
|
||||
)
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
@@ -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,
|
||||
|
||||
@@ -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())))
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user