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
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 komconfig import KomConfig
from komcache import KomCache
from komgapi import komgapi as KOMGAPI
from typing import Any, Dict, List
from limit import limit
app = Quart(__name__)
@@ -13,9 +19,6 @@ cache = KomCache()
cache.create_table(
"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()
@@ -25,35 +28,20 @@ komga = KOMGAPI(
password=settings.komga.password,
)
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
async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
@limit(90, 60)
async def fetch_data(
data: Dict[str, Any], query: str = REQUESTS_QUERY
) -> List[Dict[str, Any]]:
async with httpx.AsyncClient() as client:
try:
variables: Dict[str, Any] = {"search": data["query"]}
if len(data["genres"]) > 0:
variables: Dict[str, Any] = {"search": data["query"], "type": "MANGA"}
if data.get("genres"):
variables["genres"] = data["genres"]
if len(data["tags"]) > 0:
if data.get("tags"):
variables["tags"] = data["tags"]
print(data["query"], variables)
# print(data["query"], variables)
response = await client.post(
"https://graphql.anilist.co",
json={
@@ -62,6 +50,7 @@ async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
},
)
response.raise_for_status()
data = response.json()
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 = ?",
args=(manga.id,),
)
komga_request = False
if requested:
komga_request = True
komga_request = bool(requested)
results.append(
{
@@ -109,6 +96,73 @@ async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
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")
async def get_genres():
async with httpx.AsyncClient() as client:
@@ -147,6 +201,7 @@ async def get_tags():
@app.route("/search", methods=["POST"])
async def search():
data = await request.get_json()
print(data)
results = await fetch_data(data)
if not results:
return jsonify({"error": "No results found"}), 404
@@ -172,5 +227,21 @@ async def log_request():
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__":
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 ========== */
.results {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1em;
margin-top: 20px;
}
@@ -20,11 +21,16 @@
border: 1px solid #ccc;
background: #fafafa;
text-align: center;
transition: box-shadow 0.2s;
transition: box-shadow 0.2s transform 0.2s;
border-radius: 10px;
}
.card:hover {
box-shadow: 0 0 10px #aaa;
transform: scale(1.1);
/* pop out by 10% */
z-index: 2;
/* ensure it appears above others */
}
.card img {
@@ -39,6 +45,32 @@
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 {
position: fixed;
@@ -84,12 +116,15 @@
/* ========== BLUR CONTROL VIA BODY CLASS ========== */
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 {
filter: none;
/* Remove blur on hover, set as important */
filter: none !important;
/* Remove blur on hover */
}
@@ -145,10 +180,23 @@ body.nsfw-disabled .image-container.nsfw:hover img {
.reset {
/* 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 {
/* Implement design here */
background-color: #4CAF50;
}
.request {
@@ -183,4 +231,10 @@ body.nsfw-disabled .image-container.nsfw:hover img {
.card.komga .request {
pointer-events: none;
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>
<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='magic.css') }}">
</head>
@@ -45,17 +44,15 @@
</div>
<button onclick="window.location.href='/requests'">View Requests</button>
<button class="request-page" onclick="window.location.href='/requests'">View Requests</button>
</div>
{% if results %}
<div class="results">
{% if results %}
{% for result in results %}
<div
class="card {{ result.type | lower }} {% if result.in_komga %}komga{% endif %} {% if result.requested %}requested{% endif %}">
<div class="card {{ result.type | lower }} {% if result.in_komga %}komga{% endif %}">
<div class="image-container {{ 'nsfw' if result.isAdult else '' }}">
<img src="{{ result.image }}" alt="Cover">
@@ -74,38 +71,11 @@
</div>
</div>
{% endfor %}
{% endif %}
</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 -->
<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 selectedTags = document.getElementById("tagInput").value.split(',').map(v => v.trim()).filter(v => v);
const query = {
query: searchTerm,
genres: selectedGenres,
@@ -320,7 +292,7 @@
const card = document.createElement('div');
card.className = `card ${result.in_komga ? 'komga' : ''}`;
card.className += ` ${result.type.toLowerCase()}`;
card.className += ` ${result.requested ? 'requested' : ''}`;
// card.className += ` ${result.requested ? 'requested' : ''}`;
const imageContainer = document.createElement('div');
imageContainer.className = `image-container ${result.isAdult ? 'nsfw' : ''}`;
@@ -345,10 +317,12 @@
const infoButton = document.createElement('button');
infoButton.textContent = 'Info';
infoButton.className = 'info';
infoButton.onclick = () => showInfo(result);
const requestButton = document.createElement('button');
requestButton.textContent = 'Request';
requestButton.className = 'request';
requestButton.onclick = () => sendRequest(result.id);
// Disable request button if the result is in Komga

View File

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