From ee489b4b8d924ed6c5fe25ad92c43b174c8c5b6e Mon Sep 17 00:00:00 2001 From: Fuwn Date: Mon, 24 Jul 2023 22:27:18 -0700 Subject: feat: initial build --- src/due/__init__.py | 18 +++++ src/due/cache.py | 3 + src/due/html.py | 163 +++++++++++++++++++++++++++++++++++++++++++++ src/due/media.py | 90 +++++++++++++++++++++++++ src/due/routes/__init__.py | 0 src/due/routes/auth.py | 12 ++++ src/due/routes/index.py | 75 +++++++++++++++++++++ src/due/routes/oauth.py | 29 ++++++++ 8 files changed, 390 insertions(+) create mode 100644 src/due/__init__.py create mode 100644 src/due/cache.py create mode 100644 src/due/html.py create mode 100644 src/due/media.py create mode 100644 src/due/routes/__init__.py create mode 100644 src/due/routes/auth.py create mode 100644 src/due/routes/index.py create mode 100644 src/due/routes/oauth.py (limited to 'src/due') diff --git a/src/due/__init__.py b/src/due/__init__.py new file mode 100644 index 0000000..343e4a9 --- /dev/null +++ b/src/due/__init__.py @@ -0,0 +1,18 @@ +from flask import Flask +from due.routes import auth, oauth, index +from flask_cors import CORS +from dotenv import load_dotenv +from due.cache import cache + +load_dotenv() + +app = Flask(__name__) + +CORS(app) + +app.register_blueprint(auth.bp, url_prefix="/auth") +app.register_blueprint(oauth.bp, url_prefix="/oauth") +app.register_blueprint(index.bp) +app.secret_key = "due.moe" + +cache.init_app(app) diff --git a/src/due/cache.py b/src/due/cache.py new file mode 100644 index 0000000..6667af8 --- /dev/null +++ b/src/due/cache.py @@ -0,0 +1,3 @@ +from flask_caching import Cache + +cache = Cache(config={"CACHE_TYPE": "SimpleCache"}) diff --git a/src/due/html.py b/src/due/html.py new file mode 100644 index 0000000..8d427b1 --- /dev/null +++ b/src/due/html.py @@ -0,0 +1,163 @@ +import requests +import joblib +from due.cache import cache + + +def anime_to_html(releasing_outdated_anime): + current_html = "", len(titles)) + + +def manga_to_html(releasing_outdated_manga): + current_html = ["") + + return ("".join(current_html), len(titles)) + + +def page(main_content, footer): + return f""" + + + + 期限 + + + + + + + + + + + +

期限

+ + {main_content} + +

+ +
+ +

{footer}

+ + +""" diff --git a/src/due/media.py b/src/due/media.py new file mode 100644 index 0000000..cefc1c0 --- /dev/null +++ b/src/due/media.py @@ -0,0 +1,90 @@ +import requests + + +def user_id(anilist): + return int( + requests.post( + "https://graphql.anilist.co", + json={"query": "{ Viewer { id } }"}, + headers={ + "Authorization": anilist["token_type"] + " " + anilist["access_token"], + "Content-Type": "application/json", + "Accept": "application/json", + }, + ).json()["data"]["Viewer"]["id"] + ) + + +def create_collection(anilist, type): + current_collection = media_list_collection(anilist, type) + current = [] + + for list in current_collection["MediaListCollection"]["lists"]: + current += list["entries"] + + return (current, current_collection["MediaListCollection"]["user"]["name"]) + + +# def media_list(anilist, pageNumber): +# return requests.post( +# "https://graphql.anilist.co", +# json={"query": media_list_query(user_id(anilist), pageNumber)}, +# headers={ +# "Authorization": anilist["token_type"] + " " + anilist["access_token"], +# "Content-Type": "application/json", +# "Accept": "application/json", +# }, +# ).json()["data"] + + +# def media_list_query(user_id: int, page_number: int) -> str: +# return f"""{{ +# Page(page: {page_number}) {{ +# mediaList(userId: {user_id}, status_not_in: [COMPLETED]) {{ +# media {{ +# id +# status +# type +# title {{ romaji english }} +# nextAiringEpisode {{ episode }} +# mediaListEntry {{ progress }} +# startDate {{ year }} +# }} +# }} +# pageInfo {{ hasNextPage }} +# }} +# }}""" + + +def media_list_collection(anilist, type): + return requests.post( + "https://graphql.anilist.co", + json={"query": media_list_collection_query(user_id(anilist), type)}, + headers={ + "Authorization": anilist["token_type"] + " " + anilist["access_token"], + "Content-Type": "application/json", + "Accept": "application/json", + }, + ).json()["data"] + + +def media_list_collection_query(user_id: int, type) -> str: + return f"""{{ + MediaListCollection(userId: {user_id}, type: {"ANIME" if type == "ANIME" else "MANGA"}, status_not_in: [COMPLETED]) {{ + hasNextChunk + lists {{ + entries {{ + media {{ + id + status + type + title {{ romaji english }} + nextAiringEpisode {{ episode }} + mediaListEntry {{ progress }} + startDate {{ year }} + }} + }} + }} + user {{ name }} + }} + }}""" diff --git a/src/due/routes/__init__.py b/src/due/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/due/routes/auth.py b/src/due/routes/auth.py new file mode 100644 index 0000000..6cd1e1b --- /dev/null +++ b/src/due/routes/auth.py @@ -0,0 +1,12 @@ +from flask import redirect, Blueprint + +bp = Blueprint("auth", __name__) + + +@bp.route("/logout") +def auth_logout(): + response = redirect("/") + + response.delete_cookie("anilist") + + return response diff --git a/src/due/routes/index.py b/src/due/routes/index.py new file mode 100644 index 0000000..b4423be --- /dev/null +++ b/src/due/routes/index.py @@ -0,0 +1,75 @@ +import time +from due.html import anime_to_html, manga_to_html, page +from due.media import create_collection +from flask import request, Blueprint +import json +import os + +bp = Blueprint("index", __name__) + + +@bp.route("/") +def home(): + if request.cookies.get("anilist"): + anilist = json.loads(request.cookies.get("anilist")) + start = time.time() + (current_anime, name) = create_collection(anilist, "ANIME") + anime_time = time.time() - start + start = time.time() + (current_manga, _) = create_collection(anilist, "MANGA") + manga_time = time.time() - start + + releasing_anime = [ + media for media in current_anime if media["media"]["status"] == "RELEASING" + ] + releasing_manga = [ + media for media in current_manga if media["media"]["status"] == "RELEASING" + ] + releasing_outdated_manga = [ + media + for media in releasing_manga + if media["media"]["type"] == "MANGA" + and int(media["media"]["mediaListEntry"]["progress"]) + >= 1 # Useful when testing + ] + releasing_outdated_anime = [ + media + for media in releasing_anime + if media["media"]["type"] == "ANIME" + and int( + ( + {"episode": 0} + if media["media"]["nextAiringEpisode"] is None + else media["media"]["nextAiringEpisode"] + )["episode"] + ) + - 1 + != int(media["media"]["mediaListEntry"]["progress"]) + ] + (anime_html, anime_length) = anime_to_html(releasing_outdated_anime) + (manga_html, manga_length) = manga_to_html(releasing_outdated_manga) + + return page( + f"""Logout from AniList ({name}) + +
""", + f"""
+ Anime [{anime_length}] {round(anime_time, 2)}ms + {anime_html} +
+ +

+ +
+ Manga [{manga_length}] {round(manga_time, 2)}ms + {manga_html} +
+ """, + ) + else: + return page( + f"""Login with AniList + +
""", + "Please log in to view due media.", + ) diff --git a/src/due/routes/oauth.py b/src/due/routes/oauth.py new file mode 100644 index 0000000..290ae1d --- /dev/null +++ b/src/due/routes/oauth.py @@ -0,0 +1,29 @@ +from flask import redirect, Blueprint, request +import requests +import json +import os + +bp = Blueprint("oauth", __name__) + + +@bp.route("/callback") +def oauth_callback(): + response = redirect("/") + + response.set_cookie( + "anilist", + json.dumps( + requests.post( + "https://anilist.co/api/v2/oauth/token", + data={ + "grant_type": "authorization_code", + "client_id": os.getenv("ANILIST_CLIENT_ID"), + "client_secret": os.getenv("ANILIST_CLIENT_SECRET"), + "redirect_uri": os.getenv("ANILIST_REDIRECT_URI"), + "code": request.args.get("code"), + }, + ).json() + ), + ) + + return response -- cgit v1.2.3