add latest updates

This commit is contained in:
2025-05-11 13:49:05 +02:00
parent 11d510566a
commit 49871c1d6e
4 changed files with 176 additions and 78 deletions

View File

@@ -1,11 +1,17 @@
from quart import Quart, render_template, request, jsonify from quart import Quart, render_template, request, jsonify
import httpx import httpx
from anilistapi.queries.manga import REQUESTS_QUERY, TAGS_QUERY, GENRES_QUERY from anilistapi.queries.manga import (
REQUESTS_QUERY,
TAGS_QUERY,
GENRES_QUERY,
REQUESTED_QUERY,
)
from anilistapi.schemas.manga import Manga from anilistapi.schemas.manga import Manga
from komconfig import KomConfig from komconfig import KomConfig
from komcache import KomCache from komcache import KomCache
from komgapi import komgapi as KOMGAPI from komgapi import komgapi as KOMGAPI
from typing import Any, Dict, List from typing import Any, Dict, List
from limit import limit
app = Quart(__name__) app = Quart(__name__)
@@ -13,9 +19,6 @@ cache = KomCache()
cache.create_table( cache.create_table(
"CREATE TABLE IF NOT EXISTS manga_requests (id INTEGER PRIMARY KEY, manga_id INTEGER, grabbed BOOLEAN DEFAULT 0)" "CREATE TABLE IF NOT EXISTS manga_requests (id INTEGER PRIMARY KEY, manga_id INTEGER, grabbed BOOLEAN DEFAULT 0)"
) )
cache.create_table(
"CREATE TABLE IF NOT EXISTS manga_titles (id INTEGER PRIMARY KEY, anilist_id INTEGER DEFAULT 0, komga_title UNIQUE)"
)
settings = KomConfig() settings = KomConfig()
@@ -25,35 +28,20 @@ komga = KOMGAPI(
password=settings.komga.password, password=settings.komga.password,
) )
komga_series = komga.seriesList() komga_series = komga.seriesList()
# store the entries in the database table manga_titles, komga_series is a list of strings of the series names
for series in komga_series:
# check if the series is already in the database
existing_series = cache.fetch_one(
query="SELECT komga_title FROM manga_titles WHERE komga_title = ?",
args=(series,),
)
if existing_series:
# series already exists, skip
continue
else:
cache.insert(
# insert into if not in database
query="INSERT OR IGNORE INTO manga_titles (komga_title) VALUES (?)",
args=(series,),
)
komga_series = [series.lower() for series in komga_series]
# Update type annotations for fetch_data @limit(90, 60)
async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]: async def fetch_data(
data: Dict[str, Any], query: str = REQUESTS_QUERY
) -> List[Dict[str, Any]]:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
variables: Dict[str, Any] = {"search": data["query"]} variables: Dict[str, Any] = {"search": data["query"], "type": "MANGA"}
if len(data["genres"]) > 0: if data.get("genres"):
variables["genres"] = data["genres"] variables["genres"] = data["genres"]
if len(data["tags"]) > 0: if data.get("tags"):
variables["tags"] = data["tags"] variables["tags"] = data["tags"]
print(data["query"], variables) # print(data["query"], variables)
response = await client.post( response = await client.post(
"https://graphql.anilist.co", "https://graphql.anilist.co",
json={ json={
@@ -62,6 +50,7 @@ async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
}, },
) )
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
results: List[Dict[str, Any]] = [] results: List[Dict[str, Any]] = []
@@ -74,9 +63,7 @@ async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
query="SELECT manga_id, grabbed FROM manga_requests WHERE manga_id = ?", query="SELECT manga_id, grabbed FROM manga_requests WHERE manga_id = ?",
args=(manga.id,), args=(manga.id,),
) )
komga_request = False komga_request = bool(requested)
if requested:
komga_request = True
results.append( results.append(
{ {
@@ -109,6 +96,73 @@ async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
return [] return []
async def fetch_requested_data(data: list[int]) -> List[Dict[str, Any]]:
requested_data: list[dict[str, Any]] = []
for manga_id in data:
async with httpx.AsyncClient() as client:
try:
variables: Dict[str, Any] = {
"search": manga_id,
"type": "MANGA",
} # TODO: replace the type once the UI has been updated
# print(data, variables)
response = await client.post(
"https://graphql.anilist.co",
json={
"query": REQUESTED_QUERY,
"variables": variables,
},
)
response.raise_for_status()
data = response.json()
# print(data)
entry = data.get("data", {}).get("Media", {})
if entry:
manga = Manga(**entry)
in_komga = komga.getSeries(
manga.title.english
if manga.title.english
else manga.title.romaji
)
requested = cache.fetch_one(
query="SELECT manga_id, grabbed FROM manga_requests WHERE manga_id = ?",
args=(manga.id,),
)
komga_request = bool(requested)
requested_data.append(
{
"id": manga.id,
"title": manga.title.english
if manga.title.english
else manga.title.romaji,
"image": manga.coverImage.get("large")
if manga.coverImage
else "https://demofree.sirv.com/nope-not-here.jpg",
"status": manga.status,
"type": manga.format,
"genres": manga.genres or [],
"tags": [tag.name for tag in (manga.tags or [])],
"description": manga.description.replace("<br>", "\n")
if manga.description
else "No description available",
"isAdult": manga.isAdult,
"in_komga": in_komga,
"requested": komga_request,
}
)
except httpx.RequestError as e:
print(f"An error occurred while requesting data: {e}")
requested_data.append({"id": manga_id})
except Exception as e:
print(f"Unexpected error: {e}")
requested_data.append({"id": manga_id})
return requested_data
@app.route("/api/genres") @app.route("/api/genres")
async def get_genres(): async def get_genres():
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
@@ -147,6 +201,7 @@ async def get_tags():
@app.route("/search", methods=["POST"]) @app.route("/search", methods=["POST"])
async def search(): async def search():
data = await request.get_json() data = await request.get_json()
print(data)
results = await fetch_data(data) results = await fetch_data(data)
if not results: if not results:
return jsonify({"error": "No results found"}), 404 return jsonify({"error": "No results found"}), 404
@@ -172,5 +227,21 @@ async def log_request():
return jsonify({"status": "failed"}), 400 return jsonify({"status": "failed"}), 400
@app.route("/requests", methods=["GET"])
async def requests_page():
requests = (
cache.fetch_all(query="SELECT manga_id, grabbed FROM manga_requests") or []
)
entries: List[Dict[str, Any]] = []
req_ids = [req[0] for req in requests]
if req_ids:
entries = await fetch_requested_data(req_ids)
else:
entries = []
print(entries)
return await render_template("requests.html", requests=entries)
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5001) app.run(debug=True, host="0.0.0.0", port=5001)

View File

@@ -1,8 +1,9 @@
/* other styles are in magic.css */
/* ========== GRID LAYOUT ========== */ /* ========== GRID LAYOUT ========== */
.results { .results {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px; gap: 1em;
margin-top: 20px; margin-top: 20px;
} }
@@ -20,11 +21,16 @@
border: 1px solid #ccc; border: 1px solid #ccc;
background: #fafafa; background: #fafafa;
text-align: center; text-align: center;
transition: box-shadow 0.2s; transition: box-shadow 0.2s transform 0.2s;
border-radius: 10px;
} }
.card:hover { .card:hover {
box-shadow: 0 0 10px #aaa; box-shadow: 0 0 10px #aaa;
transform: scale(1.1);
/* pop out by 10% */
z-index: 2;
/* ensure it appears above others */
} }
.card img { .card img {
@@ -39,6 +45,32 @@
justify-content: space-around; justify-content: space-around;
} }
.actions .info {
background-color: #007bff;
color: white;
border: none;
padding: 0.5em 1em;
cursor: pointer;
border-radius: 5px;
margin-right: 0.5em;
}
.actions .info:hover {
background-color: #0056b3;
}
.actions .request {
background-color: #28a745;
color: white;
border: none;
padding: 0.5em 1em;
cursor: pointer;
border-radius: 5px;
}
.actions .request:hover {
background-color: #1e7e34;
}
/* ========== MODAL ========== */ /* ========== MODAL ========== */
.modal { .modal {
position: fixed; position: fixed;
@@ -84,12 +116,15 @@
/* ========== BLUR CONTROL VIA BODY CLASS ========== */ /* ========== BLUR CONTROL VIA BODY CLASS ========== */
body.nsfw-disabled .image-container.nsfw img { body.nsfw-disabled .image-container.nsfw img {
filter: blur(8px); filter: grayscale(100%) blur(5px) !important;
/* Ensure the blur is applied */
transition: filter 0.3s ease;
/* Add smooth transition */
} }
body.nsfw-disabled .image-container.nsfw:hover img { body.nsfw-disabled .image-container.nsfw:hover img {
filter: none; filter: none !important;
/* Remove blur on hover, set as important */ /* Remove blur on hover */
} }
@@ -145,10 +180,23 @@ body.nsfw-disabled .image-container.nsfw:hover img {
.reset { .reset {
/* Implement design here */ /* Implement design here */
background-color: #f44336;
/* round the edges of the button */
border-radius: 9px;
}
.selectors {
display: flex;
gap: 10px;
margin-bottom: 8px;
margin-top: 5px;
flex-wrap: wrap;
} }
.info { .info {
/* Implement design here */ /* Implement design here */
background-color: #4CAF50;
} }
.request { .request {
@@ -183,4 +231,10 @@ body.nsfw-disabled .image-container.nsfw:hover img {
.card.komga .request { .card.komga .request {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
}
.card.komga:hover,
.card.requested:hover {
transform: none !important;
box-shadow: none !important;
z-index: auto !important;
} }

View File

@@ -4,7 +4,6 @@
<head> <head>
<title>Anime Search and Request Page</title> <title>Anime Search and Request Page</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='magic.css') }}">
</head> </head>
@@ -45,17 +44,15 @@
</div> </div>
<button onclick="window.location.href='/requests'">View Requests</button> <button class="request-page" onclick="window.location.href='/requests'">View Requests</button>
</div> </div>
{% if results %}
<div class="results"> <div class="results">
{% if results %}
{% for result in results %} {% for result in results %}
<div <div class="card {{ result.type | lower }} {% if result.in_komga %}komga{% endif %}">
class="card {{ result.type | lower }} {% if result.in_komga %}komga{% endif %} {% if result.requested %}requested{% endif %}">
<div class="image-container {{ 'nsfw' if result.isAdult else '' }}"> <div class="image-container {{ 'nsfw' if result.isAdult else '' }}">
<img src="{{ result.image }}" alt="Cover"> <img src="{{ result.image }}" alt="Cover">
@@ -74,38 +71,11 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% endif %}
</div> </div>
{% endif %}
{% if results %}
<div class="results">
{% for result in results %}
<div
class="card {{ result.type | lower }} {% if result.in_komga %}komga{% endif %} {% if result.requested %}requested{% endif %}">
<div class="image-container {{ 'nsfw' if result.isAdult else '' }}">
<img src="{{ result.image }}" alt="Cover">
{% if result.isAdult %}
<div class="adult-badge">18+</div>
{% endif %}
</div>
<p>{{ result.title }}</p>
<div class="actions">
<button class="info" onclick="showInfo({{ result | tojson | safe }})">Info</button>
<!-- if entry has in_komga == true, do not show the request button, else show it -->
{% if not result.in_komga %}
<button class="request" onclick="sendRequest('{{ result.id }}')">Request</button>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Info Modal --> <!-- Info Modal -->
<div id="infoModal" class="modal" style="display:none;"> <div id="infoModal" class="modal" style="display:none;">
@@ -287,6 +257,8 @@
const selectedGenres = document.getElementById("genreInput").value.split(',').map(v => v.trim()).filter(v => v); const selectedGenres = document.getElementById("genreInput").value.split(',').map(v => v.trim()).filter(v => v);
const selectedTags = document.getElementById("tagInput").value.split(',').map(v => v.trim()).filter(v => v); const selectedTags = document.getElementById("tagInput").value.split(',').map(v => v.trim()).filter(v => v);
const query = { const query = {
query: searchTerm, query: searchTerm,
genres: selectedGenres, genres: selectedGenres,
@@ -320,7 +292,7 @@
const card = document.createElement('div'); const card = document.createElement('div');
card.className = `card ${result.in_komga ? 'komga' : ''}`; card.className = `card ${result.in_komga ? 'komga' : ''}`;
card.className += ` ${result.type.toLowerCase()}`; card.className += ` ${result.type.toLowerCase()}`;
card.className += ` ${result.requested ? 'requested' : ''}`; // card.className += ` ${result.requested ? 'requested' : ''}`;
const imageContainer = document.createElement('div'); const imageContainer = document.createElement('div');
imageContainer.className = `image-container ${result.isAdult ? 'nsfw' : ''}`; imageContainer.className = `image-container ${result.isAdult ? 'nsfw' : ''}`;
@@ -345,10 +317,12 @@
const infoButton = document.createElement('button'); const infoButton = document.createElement('button');
infoButton.textContent = 'Info'; infoButton.textContent = 'Info';
infoButton.className = 'info';
infoButton.onclick = () => showInfo(result); infoButton.onclick = () => showInfo(result);
const requestButton = document.createElement('button'); const requestButton = document.createElement('button');
requestButton.textContent = 'Request'; requestButton.textContent = 'Request';
requestButton.className = 'request';
requestButton.onclick = () => sendRequest(result.id); requestButton.onclick = () => sendRequest(result.id);
// Disable request button if the result is in Komga // Disable request button if the result is in Komga

View File

@@ -4,7 +4,6 @@
<head> <head>
<title>Requested Manga</title> <title>Requested Manga</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='magic.css') }}">
</head> </head>
@@ -12,13 +11,13 @@
<h1>Requested Manga</h1> <h1>Requested Manga</h1>
<div style="margin-bottom: 1em; text-align: center;"> <div style="margin-bottom: 1em; text-align: center;">
<button onclick="window.location.reload()">Refresh</button> <button class="refresh" onclick="window.location.reload()">Refresh</button>
<button onclick="window.location.href='/'">Back to Index</button> <button class="index" onclick="window.location.href='/'">Back to Index</button>
</div> </div>
{% if results %}
<div class="results"> <div class="results">
{% if results %}
{% for result in results %} {% for result in results %}
<div class="card {{ result.type | lower }} {% if result.in_komga %}komga{% else %}requested{% endif %}"> <div class="card {{ result.type | lower }} {% if result.in_komga %}komga{% else %}requested{% endif %}">
@@ -39,8 +38,8 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% endif %}
</div> </div>
{% endif %}
{% if requests %} {% if requests %}