aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--justfile4
-rw-r--r--pyproject.toml7
-rw-r--r--requirements-dev.lock8
-rw-r--r--requirements.lock8
-rw-r--r--src/oguri/__init__.py94
-rw-r--r--src/oguri/__main__.py7
-rw-r--r--src/oguri/cli.py37
-rw-r--r--src/oguri/schedule.py147
8 files changed, 212 insertions, 100 deletions
diff --git a/justfile b/justfile
index c495100..b789cfc 100644
--- a/justfile
+++ b/justfile
@@ -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"