update files, add new workflow parts, add version management to pyproject file

This commit is contained in:
2025-06-04 13:50:07 +02:00
parent ef889448bd
commit b17c0e79ee
10 changed files with 332 additions and 107 deletions

View File

@@ -22,22 +22,23 @@ log.add("application.log", rotation="1 week", retention="1 month")
log.add(sys.stdout)
app = Quart(__name__)
cache = KomCache()
cache.create_table(
"CREATE TABLE IF NOT EXISTS manga_requests (id INTEGER PRIMARY KEY, manga_id INTEGER, grabbed BOOLEAN DEFAULT 0)"
)
settings = KomConfig()
# else:
# cache.create_table(
# "CREATE TABLE IF NOT EXISTS manga_requests (id INT AUTO_INCREMENT PRIMARY KEY, anilist_id INT NOT NULL, grabbed BOOLEAN DEFAULT 0)"
komga = KOMGAPI(
url=settings.komga.url,
username=settings.komga.user,
password=settings.komga.password,
)
komga_series = komga.seriesList()
# komga_series = komga.seriesList()
@limit(90, 60)
@limit(limit=1, every=2)
async def fetch_data(
inputdata: Dict[str, Any], check_downloads: bool = False
) -> List[Dict[str, Any]]:
@@ -67,6 +68,11 @@ async def fetch_data(
"query": REQUESTS_QUERY,
"variables": variables,
},
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "KomPage Searcher API/1.0 (contact: discord-id: 908987973264089139)",
},
)
response.raise_for_status()
@@ -74,14 +80,16 @@ async def fetch_data(
log.debug(f"GraphQL response: {data}")
results: List[Dict[str, Any]] = []
cache = KomCache()
for item in data.get("data", {}).get("Page", {}).get("media", []):
manga = Manga(**item)
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,),
query="SELECT manga_id, grabbed FROM manga_requests WHERE manga_id = :id",
args={"id": manga.id},
)
komga_request = bool(requested)
results.append(
@@ -126,9 +134,12 @@ async def fetch_data(
log.error(f"Unexpected error in fetch_data: {e}")
return []
@limit(90, 60)
@limit(limit=1, every=2)
async def fetch_requested_data(data: list[int]) -> List[Dict[str, Any]]:
requested_data: list[dict[str, Any]] = []
cache = KomCache()
for manga_id in data:
async with httpx.AsyncClient() as client:
try:
@@ -158,8 +169,8 @@ async def fetch_requested_data(data: list[int]) -> List[Dict[str, Any]]:
else manga.title.romaji
)
requested = cache.fetch_one(
query="SELECT grabbed FROM manga_requests WHERE manga_id = ?",
args=(manga.id,),
query="SELECT grabbed FROM manga_requests WHERE manga_id = :id",
args={"id": manga.id},
)
komga_request = bool(requested)
@@ -281,18 +292,28 @@ async def api_search():
@app.route("/", methods=["GET"])
async def index():
return await render_template("index.html", komga_series=komga_series)
return await render_template("index.html") # , komga_series=komga_series)
@app.route("/request", methods=["POST"])
async def log_request():
data = await request.get_json()
log.debug(f"Received request data: {data}")
item = data.get("item")
if item:
data = await fetch_requested_data([item])
if not data:
return jsonify({"status": "failed", "message": "Item not found"}), 404
data = data[0]
title = data.get("title")
image_url = data.get("image")
log.debug(
f"Logging request for item: {item}, title: {title}, image_url: {image_url}"
)
asynccache = KomCache()
asynccache.insert(
"INSERT INTO manga_requests (manga_id) VALUES (?)",
(item,),
f"INSERT INTO manga_requests (manga_id, title, image) VALUES ({item}, '{title}', :image)",
args={"image": image_url},
)
return jsonify({"status": "success"})
return jsonify({"status": "failed"}), 400
@@ -300,21 +321,42 @@ async def log_request():
@app.route("/requests", methods=["GET"])
async def requests_page():
cache = KomCache()
requests = (
cache.fetch_all(query="SELECT manga_id, grabbed FROM manga_requests") or []
cache.fetch_all(
query="SELECT manga_id, title, image 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)
entries = [
{"manga_id": req[0], "title": req[1], "image": req[2]} for req in requests
]
else:
entries = []
data = []
for entry in entries:
data.append(dict(entry))
return await render_template("requests.html", requests=data)
return await render_template("requests.html", requests=entries)
@app.route("/delete", methods=["POST"])
async def delete_request():
# Delete a request from the database. ID is sent after the /delete endpoint, so: /delete/<id>
data = await request.get_json()
item_id = data.get("item")
if item_id:
asynccache = KomCache()
asynccache.query(
"DELETE FROM manga_requests WHERE manga_id = :id", args={"id": item_id}
)
return jsonify({"status": "success"})
return jsonify({"status": "failed"}), 400
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5001)
log.info("Starting Komga Manga Grabber API")
# use hypercorn to run the app
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=5001)
# run in test mode

View File

@@ -55,6 +55,7 @@
margin-right: 0.5em;
}
.actions .info:hover {
background-color: #0056b3;
}
@@ -230,4 +231,21 @@ body.nsfw-disabled .image-container.nsfw:hover img {
transform: none !important;
box-shadow: none !important;
z-index: auto !important;
}
.card .delete-button {
position: absolute;
top: 5px;
right: 5px;
background-color: #dc3545;
color: white;
border: none;
padding: 0.5em;
cursor: pointer;
border-radius: 50%;
z-index: 3;
opacity: 1;
}
.card:hover .delete-button {
display: block;
}

View File

@@ -77,7 +77,8 @@
<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>
<button class="request" onclick="sendRequest({{ result | tojson | safe }})">Request</button>
{% endif %}
</div>
</div>
@@ -113,7 +114,9 @@
fetch("/request", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ item: item })
body: JSON.stringify({
item: item
})
})
.then(res => res.json())
.then(data => alert(data.status === "success" ? "Request logged!" : "Failed"));
@@ -304,7 +307,7 @@
const card = document.createElement('div');
card.className = `card ${result.in_komga ? 'komga' : ''}`;
card.className += ` ${result.type.toLowerCase()}`;
// card.className += ` ${result.requested ? 'requested' : ''}`;
const imageContainer = document.createElement('div');
imageContainer.className = `image-container ${result.isAdult ? 'nsfw' : ''}`;
@@ -335,14 +338,16 @@
const requestButton = document.createElement('button');
requestButton.textContent = 'Request';
requestButton.className = 'request';
requestButton.onclick = () => sendRequest(result.id);
requestButton.onclick = () => sendRequest(result);
// Disable request button if the result is in Komga
if (result.in_komga) {
requestButton.disabled = true;
}
if (result.requested) {
requestButton.textContent = 'Requested';
requestButton.disabled = true;
card.style.border = '3px solid orange';
}
actions.appendChild(infoButton);

View File

@@ -4,6 +4,7 @@
<head>
<title>Requested Manga</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
@@ -14,12 +15,38 @@
<button class="index" onclick="window.location.href='/'">Back to Index</button>
</div>
<div class="results">
{% if requests %}
{% if results %}
{% for result in results %}
<div class="card {{ result.type | lower }} {% if result.in_komga %}komga{% else %}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 onclick="showInfo({{ result | tojson | safe }})" class="info">Info</button>
<!-- if entry has in_komga == true, do not show the request button, else show it -->
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% if requests %}
<div class="results">
{% for request in requests %}
<div class="card {{ request.type | lower }} {% if request.in_komga %}komga{% else %}requested{% endif %}"
data-info="{{ request | tojson }}">
<div class="card {{ request.type | lower }} {% if request.in_komga %}komga{% else %}requested{% endif %}">
<div class="image-container {{ 'nsfw' if request.isAdult else '' }}">
<img src="{{ request.image }}" alt="Cover">
{% if request.isAdult %}
<div class="adult-badge">18+</div>
@@ -28,17 +55,19 @@
<p>{{ request.title }}</p>
<div class="actions">
<button class="info">Info</button>
<button onclick="showInfo({{ request | tojson | safe }})" class="info">Info</button>
<button onclick="deleteEntry({{ request.id }})" class="delete-button">Delete</button>
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% if not requests %}
<p>No requests found.</p>
{% else %}
<p>No requests found. This may be because the database disconnected. Please refresh using the button, if the message
persists, there are no requests</p>
{% endif %}
<!-- Info Modal -->
<div id="infoModal" class="modal" style="display:none;">
<div class="modal-content">
<span class="close" onclick="closeModal()">&times;</span>
@@ -54,25 +83,6 @@
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
// Add click event listeners to all info buttons
document.querySelectorAll('.card .info').forEach(button => {
button.addEventListener('click', (e) => {
// Get the parent card element
const card = e.target.closest('.card');
// Get the data from the data-info attribute
try {
const data = JSON.parse(card.dataset.info);
console.log("Info button clicked, data:", data);
showInfo(data);
} catch (error) {
console.error("Error parsing data:", error);
console.error("Raw data:", card.dataset.info);
}
});
});
});
function showInfo(data) {
console.log("showInfo data:", data);
try {
@@ -101,6 +111,28 @@
closeModal();
}
}
function deleteEntry(entryId) {
fetch(`/delete`, {
method: 'POST',
body: JSON.stringify({ item: entryId }),
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (response.ok) {
alert('Entry deleted successfully!');
window.location.reload(); // Refresh the page to update the list
} else {
alert('Failed to delete the entry.');
}
})
.catch(error => {
console.error('Error deleting entry:', error);
alert('An error occurred while deleting the entry.');
});
}
</script>
</body>