diff options
| author | Fuwn <[email protected]> | 2023-07-24 22:27:18 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2023-07-24 22:27:18 -0700 |
| commit | ee489b4b8d924ed6c5fe25ad92c43b174c8c5b6e (patch) | |
| tree | ec425b61704100e1708e9637c3c967ee6b77e683 /src | |
| download | old.due.moe-ee489b4b8d924ed6c5fe25ad92c43b174c8c5b6e.tar.xz old.due.moe-ee489b4b8d924ed6c5fe25ad92c43b174c8c5b6e.zip | |
feat: initial build
Diffstat (limited to 'src')
| -rw-r--r-- | src/__init__.py | 0 | ||||
| -rw-r--r-- | src/due/__init__.py | 18 | ||||
| -rw-r--r-- | src/due/cache.py | 3 | ||||
| -rw-r--r-- | src/due/html.py | 163 | ||||
| -rw-r--r-- | src/due/media.py | 90 | ||||
| -rw-r--r-- | src/due/routes/__init__.py | 0 | ||||
| -rw-r--r-- | src/due/routes/auth.py | 12 | ||||
| -rw-r--r-- | src/due/routes/index.py | 75 | ||||
| -rw-r--r-- | src/due/routes/oauth.py | 29 |
9 files changed, 390 insertions, 0 deletions
diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/__init__.py 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 = "<ul>" + titles = [] + + for media in releasing_outdated_anime: + anime = media["media"] + title = anime["title"]["english"] + + if title in titles: + continue + else: + titles.append(title) + + id = anime["id"] + progress = anime["mediaListEntry"]["progress"] + available = ( + {"episode": 0} + if media["media"]["nextAiringEpisode"] is None + else media["media"]["nextAiringEpisode"] + )["episode"] - 1 + + if available <= 0: + available = "?" + + if title is None: + title = anime["title"]["romaji"] + + current_html += f'<li><a href="https://anilist.co/anime/{id}">{title}</a> {progress} [{available}]</li>' + + return (current_html + "</ul>", len(titles)) + + +def manga_to_html(releasing_outdated_manga): + current_html = ["<ul>"] + titles = [] + + def process(media): + manga = media["media"] + title = manga["title"]["english"] + + if title in titles: + return + else: + titles.append(title) + + id = manga["id"] + progress = manga["mediaListEntry"]["progress"] + available = ( + {"episode": 0} + if media["media"]["nextAiringEpisode"] is None + else media["media"]["nextAiringEpisode"] + )["episode"] - 1 + + if available <= 0: + available = "?" + + if title is None: + title = manga["title"]["romaji"] + + mangadex_data = cache.get(str(manga["id"]) + "id") + mangadex_id = None + + if mangadex_data is None: + mangadex_data = requests.get( + "https://api.mangadex.org/manga", + params={"title": title, "year": manga["startDate"]["year"]}, + ).json()["data"] + + cache.set(str(manga["id"]) + "id", mangadex_data) + + if len(mangadex_data) == 0: + available = "?" + else: + mangadex_id = mangadex_data[0]["id"] + manga_chapter_aggregate = cache.get(str(manga["id"]) + "ag") + + if manga_chapter_aggregate is None: + manga_chapter_aggregate = requests.get( + f"https://api.mangadex.org/manga/{mangadex_id}/aggregate", + ).json() + + cache.set(str(manga["id"]) + "ag", manga_chapter_aggregate) + + if "none" in manga_chapter_aggregate["volumes"]: + available = list( + manga_chapter_aggregate["volumes"]["none"]["chapters"] + )[0] + + if str(available) == "none": + available = list( + manga_chapter_aggregate["volumes"]["none"]["chapters"] + )[1] + else: + try: + available = list( + manga_chapter_aggregate["volumes"][ + str(len(list(manga_chapter_aggregate["volumes"])) - 1) + ]["chapters"] + )[0] + except Exception: + available = "?" + + if str(progress) == str(available): + titles.pop() + + return + + if str(available)[0] == "{": + return + + available_link = ( + available + if mangadex_id is None + else f'<a href="https://mangadex.org/title/{mangadex_id}">{available}</a>' + ) + + current_html.append( + f'<li><a href="https://anilist.co/manga/{id}">{title}</a> {progress} [{available_link}]</li>' + ) + + joblib.Parallel(n_jobs=80, require="sharedmem")( + joblib.delayed(process)(media) for media in releasing_outdated_manga + ) + + current_html.append("</ul>") + + return ("".join(current_html), len(titles)) + + +def page(main_content, footer): + return f""" +<!DOCTYPE html> +<html> + <head> + <title>期限</title> + + <link rel="stylesheet" type="text/css" href="https://latex.now.sh/style.css"> + <link rel="stylesheet" type="text/css" href="https://skybox.sh/css/palettes/base16-light.css"> + <link rel="stylesheet" type="text/css" href="https://skybox.sh/css/risotto.css"> + <!-- <link rel="stylesheet" type="text/css" href="https://skybox.sh/css/custom.css"> --> + <link rel="icon" type="image/png" href="https://ps.fuwn.me/-wLj4vfbxrc/favicon.png"> + </head> + + <body> + <style>text-align: center;</style> + + <h1>期限</h1> + + {main_content} + + <p></p> + + <hr> + + <p>{footer}</p> + </body> +</html> +""" 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 --- /dev/null +++ b/src/due/routes/__init__.py 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__) + + [email protected]("/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__) + + [email protected]("/") +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"""<a href="/auth/logout">Logout from AniList ({name})</a> + + <br>""", + f"""<details open> + <summary>Anime [{anime_length}] <small style="opacity: 50%">{round(anime_time, 2)}ms</small></summary> + {anime_html} + </details> + + <p></p> + + <details open> + <summary>Manga [{manga_length}] <small style="opacity: 50%">{round(manga_time, 2)}ms</small></summary> + {manga_html} + </details> + """, + ) + else: + return page( + f"""<a href="https://anilist.co/api/v2/oauth/authorize?client_id={os.getenv('ANILIST_CLIENT_ID')}&redirect_uri={os.getenv('ANILIST_REDIRECT_URI')}&response_type=code">Login with AniList</a> + + <br>""", + "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__) + + [email protected]("/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 |