aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2023-07-24 22:27:18 -0700
committerFuwn <[email protected]>2023-07-24 22:27:18 -0700
commitee489b4b8d924ed6c5fe25ad92c43b174c8c5b6e (patch)
treeec425b61704100e1708e9637c3c967ee6b77e683 /src
downloadold.due.moe-ee489b4b8d924ed6c5fe25ad92c43b174c8c5b6e.tar.xz
old.due.moe-ee489b4b8d924ed6c5fe25ad92c43b174c8c5b6e.zip
feat: initial build
Diffstat (limited to 'src')
-rw-r--r--src/__init__.py0
-rw-r--r--src/due/__init__.py18
-rw-r--r--src/due/cache.py3
-rw-r--r--src/due/html.py163
-rw-r--r--src/due/media.py90
-rw-r--r--src/due/routes/__init__.py0
-rw-r--r--src/due/routes/auth.py12
-rw-r--r--src/due/routes/index.py75
-rw-r--r--src/due/routes/oauth.py29
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__)
+
+
+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__)
+
+
+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