Merge pull request 'dev' (#9) from dev into main

Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
2025-05-05 07:06:55 +01:00
5 changed files with 127 additions and 17 deletions

View File

@@ -0,0 +1,51 @@
name: Bug Report
description: Report a bug in this project
labels:
- kind/bug
- triage
body:
- name: Bug Report
description: |
Please fill out the following sections to help us understand the bug.
If you have screenshots or code snippets, please include them.
- type: textarea
id: bug
attributes:
label: Describe the bug
description: |
A clear and concise description of what the bug is.
What did you expect to happen? What happened instead?
Include screenshots if applicable.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: |
A clear and concise description of how to reproduce the bug.
Include steps, code snippets, or screenshots if applicable.
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional information
description: |
Add any other context or screenshots about the bug here.
- type: dropdown
id: severity
attributes:
label: Severity
description: |
Select the severity of the bug.
options:
- label: Critical
value: critical
- label: Major
value: major
- label: Minor
value: minor
validations:
required: true

View File

@@ -8,6 +8,7 @@ dependencies = [
"flask>=3.1.0", "flask>=3.1.0",
"httpx>=0.28.1", "httpx>=0.28.1",
"jinja2>=3.1.6", "jinja2>=3.1.6",
"komgapi",
"quart>=0.20.0", "quart>=0.20.0",
] ]
@@ -15,3 +16,4 @@ dependencies = [
anilistapi = { workspace = true } anilistapi = { workspace = true }
komconfig = { workspace = true } komconfig = { workspace = true }
komcache = { workspace = true } komcache = { workspace = true }
komgapi = { workspace = true }

View File

@@ -5,6 +5,7 @@ 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
app = Quart(__name__) app = Quart(__name__)
@@ -12,6 +13,9 @@ 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()
@@ -20,13 +24,31 @@ komga = KOMGAPI(
username=settings.komga.user, username=settings.komga.user,
password=settings.komga.password, password=settings.komga.password,
) )
komga_books = komga.bookList() 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]
async def fetch_data(data): # Update type annotations for fetch_data
async def fetch_data(data: Dict[str, Any]) -> List[Dict[str, Any]]:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
variables = {"search": data["query"]} variables: Dict[str, Any] = {"search": data["query"]}
if len(data["genres"]) > 0: if len(data["genres"]) > 0:
variables["genres"] = data["genres"] variables["genres"] = data["genres"]
if len(data["tags"]) > 0: if len(data["tags"]) > 0:
@@ -42,10 +64,13 @@ async def fetch_data(data):
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
results = [] results: List[Dict[str, Any]] = []
for item in data.get("data", {}).get("Page", {}).get("media", []): for item in data.get("data", {}).get("Page", {}).get("media", []):
manga = Manga(**item) manga = Manga(**item)
in_komga = komga.getSeries(
manga.title.english if manga.title.english else manga.title.romaji
)
print(in_komga, manga.title.english)
results.append( results.append(
{ {
"id": manga.id, "id": manga.id,
@@ -63,6 +88,7 @@ async def fetch_data(data):
if manga.description if manga.description
else "No description available", else "No description available",
"isAdult": manga.isAdult, "isAdult": manga.isAdult,
"in_komga": in_komga,
} }
) )
@@ -122,13 +148,13 @@ async def search():
@app.route("/", methods=["GET"]) @app.route("/", methods=["GET"])
async def index(): async def index():
return await render_template("index.html") return await render_template("index.html", komga_series=komga_series)
@app.route("/request", methods=["POST"]) @app.route("/request", methods=["POST"])
async def log_request(): async def log_request():
data = await request.get_json() data = await request.get_json()
item = data.get("item") item = data.get("title")
if item: if item:
asynccache = KomCache() asynccache = KomCache()
manga_title = data.get("title") manga_title = data.get("title")

View File

@@ -154,3 +154,12 @@ body.nsfw-disabled .image-container.nsfw:hover img {
.request { .request {
/* Implement design here */ /* Implement design here */
} }
.card.komga {
border: 3px solid green;
box-sizing: border-box;
/* disable the request button for entries in Komga */
pointer-events: none;
opacity: 0.5;
}

View File

@@ -14,17 +14,17 @@
<input type="text" id="searchInput" placeholder="Search..." /> <input type="text" id="searchInput" placeholder="Search..." />
<div class="selectors"> <div class="selectors">
<div class="autocomplete"> <div class="autocomplete">
<input type="text" id="genreInput" placeholder="Select genres..." oninput="filterSuggestions('genre')" <input type="text" id="genreInput" placeholder="Select genres..." readonly
onclick="showDropdown('genre')"> oninput="filterSuggestions('genre')" onclick="showDropdown('genre')">
<button onclick="resetGenres()" , class="reset">Reset</button> <button onclick="resetGenres()" class="reset">Reset</button>
<div class="dropdown-content" id="genreDropdown"> <div class="dropdown-content" id="genreDropdown">
<!-- Genre suggestions will be dynamically populated here --> <!-- Genre suggestions will be dynamically populated here -->
</div> </div>
</div> </div>
<div class="autocomplete"> <div class="autocomplete">
<input type="text" id="tagInput" placeholder="Select tags..." oninput="filterSuggestions('tag')" <input type="text" id="tagInput" placeholder="Select tags..." readonly
onclick="showDropdown('tag')"> oninput="filterSuggestions('tag')" onclick="showDropdown('tag')">
<button onclick="resetTags()" class="reset">Reset</button> <button onclick="resetTags()" class="reset">Reset</button>
<div class="dropdown-content" id="tagDropdown"> <div class="dropdown-content" id="tagDropdown">
<!-- Tag suggestions will be dynamically populated here --> <!-- Tag suggestions will be dynamically populated here -->
@@ -44,7 +44,8 @@
<div class="results"> <div class="results">
{% for result in results %} {% for result in results %}
<div class="card">
<div class="card {% if result.in_komga %}komga{% 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">
@@ -56,7 +57,10 @@
<p>{{ result.title }}</p> <p>{{ result.title }}</p>
<div class="actions"> <div class="actions">
<button onclick="showInfo({{ result | tojson | safe }})" , class="info">Info</button> <button onclick="showInfo({{ result | tojson | safe }})" , class="info">Info</button>
<button onclick="sendRequest({{ result.id | tojson }})" , class="request">Request</button> <!-- if entry has in_komga == true, do not show the request button, else show it -->
{% if not result.in_komga %}
<button onclick="sendRequest('{{ result.id }}')" class="request">Request</button>
{% endif %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@@ -129,6 +133,8 @@
fetchOptions("/api/tags", "tag"); fetchOptions("/api/tags", "tag");
}); });
async function fetchOptions(url, dropdownId) { async function fetchOptions(url, dropdownId) {
try { try {
const res = await fetch(url); const res = await fetch(url);
@@ -151,10 +157,15 @@
const input = document.getElementById(inputId + 'Input'); const input = document.getElementById(inputId + 'Input');
const currentValues = input.value.split(',').map(v => v.trim()).filter(v => v); const currentValues = input.value.split(',').map(v => v.trim()).filter(v => v);
// Add the selected value if it's not already in the list
if (!currentValues.includes(value)) { if (!currentValues.includes(value)) {
currentValues.push(value); currentValues.push(value);
input.value = currentValues.join(', '); input.value = currentValues.join(', ');
} }
// Hide the dropdown after selection
const dropdown = document.getElementById(inputId + 'Dropdown');
dropdown.classList.remove('show');
} }
function filterSuggestions(inputId) { function filterSuggestions(inputId) {
@@ -172,6 +183,13 @@
}); });
dropdown.classList.toggle('show', hasVisibleItems); dropdown.classList.toggle('show', hasVisibleItems);
// Reset visibility of all items if input is cleared
if (!filter) {
Array.from(items).forEach(item => {
item.style.display = '';
});
}
} }
function showDropdown(inputId) { function showDropdown(inputId) {
@@ -188,11 +206,15 @@
}); });
function resetGenres() { function resetGenres() {
document.getElementById('genreInput').value = ''; const input = document.getElementById('genreInput');
input.value = '';
filterSuggestions('genre'); // Reset dropdown to show all
} }
function resetTags() { function resetTags() {
document.getElementById('tagInput').value = ''; const input = document.getElementById('tagInput');
input.value = '';
filterSuggestions('tag'); // Reset dropdown to show all
} }
function performSearch() { function performSearch() {
@@ -222,7 +244,7 @@
data.forEach(result => { data.forEach(result => {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'card'; card.className = `card ${result.in_komga ? 'komga' : ''}`;
const imageContainer = document.createElement('div'); const imageContainer = document.createElement('div');
imageContainer.className = `image-container ${result.isAdult ? 'nsfw' : ''}`; imageContainer.className = `image-container ${result.isAdult ? 'nsfw' : ''}`;