update files, add new workflow parts, add version management to pyproject file
This commit is contained in:
84
src/app.py
84
src/app.py
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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()">×</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user