diff options
| -rw-r--r-- | justfile | 4 | ||||
| -rw-r--r-- | pyproject.toml | 7 | ||||
| -rw-r--r-- | requirements-dev.lock | 8 | ||||
| -rw-r--r-- | requirements.lock | 8 | ||||
| -rw-r--r-- | src/oguri/__init__.py | 94 | ||||
| -rw-r--r-- | src/oguri/__main__.py | 7 | ||||
| -rw-r--r-- | src/oguri/cli.py | 37 | ||||
| -rw-r--r-- | src/oguri/schedule.py | 147 |
8 files changed, 212 insertions, 100 deletions
@@ -4,8 +4,8 @@ alias fmt := format generate target="client": rye run ariadne-codegen {{ target }} -run: - rye run oguri +run *arguments: + rye run oguri {{ arguments }} format: rye run black src/oguri diff --git a/pyproject.toml b/pyproject.toml index 13c5faa..22be74a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,14 @@ version = "0.1.0" description = "Command-line tool for AniList" authors = [{ name = "Fuwn", email = "[email protected]" }] dependencies = [ - "ariadne-codegen @ git+https://github.com/flonou/ariadne-codegen@field_names_should_not_be_converted_to_snake-case", - "asyncio>=3.4.3", + "ariadne-codegen @ git+https://github.com/flonou/ariadne-codegen@field_names_should_not_be_converted_to_snake-case", + "asyncio>=3.4.3", + "rich>=14.0.0", ] requires-python = ">= 3.13" [project.scripts] -"oguri" = "oguri:main_script" +"oguri" = "oguri.cli:main_script" [build-system] requires = ["hatchling"] diff --git a/requirements-dev.lock b/requirements-dev.lock index bfd2dc1..e8e57cc 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -41,6 +41,10 @@ idna==3.10 # via httpx isort==6.0.1 # via ariadne-codegen +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py mypy-extensions==1.1.0 # via black packaging==25.0 @@ -55,6 +59,10 @@ pydantic-core==2.33.2 # via pydantic pyflakes==3.4.0 # via autoflake +pygments==2.19.2 + # via rich +rich==14.0.0 + # via oguri sniffio==1.3.1 # via anyio toml==0.10.2 diff --git a/requirements.lock b/requirements.lock index bfd2dc1..e8e57cc 100644 --- a/requirements.lock +++ b/requirements.lock @@ -41,6 +41,10 @@ idna==3.10 # via httpx isort==6.0.1 # via ariadne-codegen +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py mypy-extensions==1.1.0 # via black packaging==25.0 @@ -55,6 +59,10 @@ pydantic-core==2.33.2 # via pydantic pyflakes==3.4.0 # via autoflake +pygments==2.19.2 + # via rich +rich==14.0.0 + # via oguri sniffio==1.3.1 # via anyio toml==0.10.2 diff --git a/src/oguri/__init__.py b/src/oguri/__init__.py index 3720668..3e66273 100644 --- a/src/oguri/__init__.py +++ b/src/oguri/__init__.py @@ -1,92 +1,4 @@ -from anilist_client import Client -from anilist_client.custom_fields import ( - AiringScheduleFields, - MediaFields, - MediaTitleFields, - PageFields, -) -from anilist_client.custom_queries import Query -from datetime import datetime +from .cli import main_script - -async def main() -> int: - client = Client(url="https://graphql.anilist.co") - airing_schedules_query = Query.page().fields( - PageFields.airing_schedules().fields( - AiringScheduleFields.airing_at, - AiringScheduleFields.episode, - AiringScheduleFields.media().fields( - MediaFields.title().fields( - MediaTitleFields.english(), - MediaTitleFields.romaji(), - MediaTitleFields.native(), - ) - ), - ) - ) - - try: - response = await client.query( - airing_schedules_query, operation_name="get_airing_schedules" - ) - - if response: - page = response.get("Page") - - if page: - airing_schedules = page.get("airingSchedules") - - if airing_schedules: - for schedule in airing_schedules: - airing_at = schedule.get("airingAt") - episode = schedule.get("episode") - titles = schedule.get("media").get("title") - title = ( - titles.get("english") - or titles.get("romaji") - or titles.get("native") - ) - - if airing_at: - airing_at_date = datetime.fromtimestamp(airing_at) - relative_airing_at = relative_time(airing_at_date) - to_print = f"{title} Ep. {episode} " - - if datetime.now() > airing_at_date: - to_print += f"has already aired {relative_airing_at}" - else: - to_print += f"is airing at {airing_at_date}" - - print(to_print) - except Exception as exception: - print(exception) - - return 1 - - return 0 - - -def relative_time(date): - now = datetime.now() - delta = now - date - - if delta.days > 365: - return f"{delta.days // 365} years ago" - elif delta.days > 30: - return f"{delta.days // 30} months ago" - elif delta.days > 7: - return f"{delta.days // 7} weeks ago" - elif delta.days > 1: - return f"{delta.days} days ago" - elif delta.seconds > 3600: - return f"{delta.seconds // 3600} hours ago" - elif delta.seconds > 60: - return f"{delta.seconds // 60} minutes ago" - else: - return f"{delta.seconds} seconds ago" - - -def main_script(): - import asyncio - - asyncio.run(main()) +if __name__ == "__main__": + main_script() diff --git a/src/oguri/__main__.py b/src/oguri/__main__.py index 7b4d22f..6cda171 100644 --- a/src/oguri/__main__.py +++ b/src/oguri/__main__.py @@ -1,5 +1,4 @@ -import oguri -import sys -import asyncio +from oguri.cli import main_script -sys.exit(asyncio.run(oguri.main())) +if __name__ == "__main__": + main_script() diff --git a/src/oguri/cli.py b/src/oguri/cli.py new file mode 100644 index 0000000..a9b010b --- /dev/null +++ b/src/oguri/cli.py @@ -0,0 +1,37 @@ +import asyncio +import click +from . import schedule as schedule_logic + + +def cli(): + """A command-line tool for AniList.""" + + pass + + [email protected]("day", required=False, default="today") [email protected]("--reverse", is_flag=True, help="Reverse the order of the schedule.") +def schedule(day, reverse): + """ + Shows the airing schedule for a given day. + + DAY can be 'today', 'tomorrow', or an integer representing the number of days from now. + """ + + days_offset = 0 + + if day == "tomorrow": + days_offset = 1 + elif day != "today": + try: + days_offset = int(day) + except ValueError: + raise click.BadParameter('DAY must be "today", "tomorrow", or an integer.') + + asyncio.run(schedule_logic.show_schedule(days_offset, reverse)) + + +def main_script(): + cli() diff --git a/src/oguri/schedule.py b/src/oguri/schedule.py new file mode 100644 index 0000000..cbd0d2a --- /dev/null +++ b/src/oguri/schedule.py @@ -0,0 +1,147 @@ +from datetime import datetime, timedelta +from rich.console import Console +from rich.table import Table +from anilist_client import Client +from anilist_client.custom_fields import ( + AiringScheduleFields, + MediaFields, + MediaTitleFields, + PageFields, +) +from anilist_client.custom_queries import Query + + +async def show_schedule(days_offset: int, reverse_order: bool = False): + client = Client(url="https://graphql.anilist.co") + start_of_day = datetime.now().replace( + hour=0, minute=0, second=0, microsecond=0 + ) + timedelta(days=days_offset) + end_of_day = start_of_day.replace(hour=23, minute=59, second=59, microsecond=999999) + airing_schedules_query = Query.page().fields( + PageFields.airing_schedules( + airing_at_greater=int(start_of_day.timestamp()), + episode=1, + airing_at_lesser=int(end_of_day.timestamp()), + ).fields( + AiringScheduleFields.airing_at, + AiringScheduleFields.episode, + AiringScheduleFields.media().fields( + MediaFields.site_url, + MediaFields.title().fields( + MediaTitleFields.english(), + MediaTitleFields.romaji(), + MediaTitleFields.native(), + ), + ), + ) + ) + + try: + response = await client.query( + airing_schedules_query, operation_name="get_airing_schedules" + ) + + if response: + page = response.get("Page") + + if page: + airing_schedules = page.get("airingSchedules") + + if airing_schedules: + if reverse_order: + airing_schedules.sort(key=lambda x: x.get("airingAt")) + else: + airing_schedules.sort( + key=lambda x: x.get("airingAt"), reverse=True + ) + + console = Console() + table = Table(show_header=True, header_style="bold magenta") + + table.add_column("Title") + table.add_column("Episode") + table.add_column("Airing Time") + + for schedule in airing_schedules: + airing_at = schedule.get("airingAt") + episode = schedule.get("episode") + media = schedule.get("media") + site_url = media.get("siteUrl") + titles = media.get("title") + title = ( + titles.get("english") + or titles.get("romaji") + or titles.get("native") + ) + + if airing_at: + airing_at_date = datetime.fromtimestamp(airing_at) + airing_time_string = "" + + if datetime.now() > airing_at_date: + airing_time_string = ( + f"Aired {relative_time(airing_at_date)}" + ) + else: + airing_time_string = format_future_airing_time( + airing_at_date + ) + + table.add_row( + f"[link={site_url}]{title}[/link]", + str(episode), + airing_time_string, + ) + + console.print(table) + except Exception as exception: + print(exception) + + +def format_future_airing_time(date): + now = datetime.now() + delta = date - now + hours, remainder = divmod(int(delta.total_seconds()), 3600) + minutes, _ = divmod(remainder, 60) + time_str = date.strftime("%I:%M %p") + parts = [] + + if hours > 0: + parts.append(f"{hours}h") + if minutes > 0: + parts.append(f"{minutes}m") + + if not parts: + return f"Airing soon at {time_str}" + + return f"In {' '.join(parts)} at {time_str}" + + +def relative_time(date): + now = datetime.now() + delta = now - date + + if delta.days > 365: + years = delta.days // 365 + + return f"{years} year{'s' if years > 1 else ''} ago" + elif delta.days > 30: + months = delta.days // 30 + + return f"{months} month{'s' if months > 1 else ''} ago" + elif delta.days > 7: + weeks = delta.days // 7 + + return f"{weeks} week{'s' if weeks > 1 else ''} ago" + elif delta.days > 1: + return f"{delta.days} day{'s' if delta.days > 1 else ''} ago" + elif delta.seconds > 3600: + hours = delta.seconds // 3600 + + return f"{hours} hour{'s' if hours > 1 else ''} ago" + elif delta.seconds > 60: + minutes = delta.seconds // 60 + + return f"{minutes} minute{'s' if minutes > 1 else ''} ago" + else: + return f"{delta.seconds} second{'s' if delta.seconds > 1 else ''} ago" |