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:
51
.gitea/ISSUE_TEMPLATE/bug.yml
Normal file
51
.gitea/ISSUE_TEMPLATE/bug.yml
Normal 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
|
||||||
|
|
||||||
@@ -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 }
|
||||||
|
|||||||
40
src/app.py
40
src/app.py
@@ -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")
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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' : ''}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user