changes to series order system. add artist retagging tool
This commit is contained in:
+126
@@ -537,11 +537,13 @@ def search_tags():
|
|||||||
- q: search query (matches tag name, case-insensitive)
|
- q: search query (matches tag name, case-insensitive)
|
||||||
- limit: max results (default 10)
|
- limit: max results (default 10)
|
||||||
- exclude_kind: comma-separated list of tag kinds to exclude (e.g., 'archive')
|
- exclude_kind: comma-separated list of tag kinds to exclude (e.g., 'archive')
|
||||||
|
- kind: filter to only include tags of this kind (e.g., 'series')
|
||||||
"""
|
"""
|
||||||
query = request.args.get('q', '').strip().lower()
|
query = request.args.get('q', '').strip().lower()
|
||||||
limit = request.args.get('limit', 10, type=int)
|
limit = request.args.get('limit', 10, type=int)
|
||||||
exclude_kind = request.args.get('exclude_kind', '').strip()
|
exclude_kind = request.args.get('exclude_kind', '').strip()
|
||||||
exclude_kinds = [k.strip() for k in exclude_kind.split(',') if k.strip()]
|
exclude_kinds = [k.strip() for k in exclude_kind.split(',') if k.strip()]
|
||||||
|
include_kind = request.args.get('kind', '').strip()
|
||||||
|
|
||||||
if not query:
|
if not query:
|
||||||
# Return most-used tags when no query
|
# Return most-used tags when no query
|
||||||
@@ -552,6 +554,8 @@ def search_tags():
|
|||||||
)
|
)
|
||||||
if exclude_kinds:
|
if exclude_kinds:
|
||||||
base_query = base_query.filter(~Tag.kind.in_(exclude_kinds))
|
base_query = base_query.filter(~Tag.kind.in_(exclude_kinds))
|
||||||
|
if include_kind:
|
||||||
|
base_query = base_query.filter(Tag.kind == include_kind)
|
||||||
tags = (
|
tags = (
|
||||||
base_query
|
base_query
|
||||||
.order_by(func.count(image_tags.c.image_id).desc())
|
.order_by(func.count(image_tags.c.image_id).desc())
|
||||||
@@ -562,6 +566,8 @@ def search_tags():
|
|||||||
base_query = Tag.query.filter(Tag.name.ilike(f'%{query}%'))
|
base_query = Tag.query.filter(Tag.name.ilike(f'%{query}%'))
|
||||||
if exclude_kinds:
|
if exclude_kinds:
|
||||||
base_query = base_query.filter(~Tag.kind.in_(exclude_kinds))
|
base_query = base_query.filter(~Tag.kind.in_(exclude_kinds))
|
||||||
|
if include_kind:
|
||||||
|
base_query = base_query.filter(Tag.kind == include_kind)
|
||||||
tags = (
|
tags = (
|
||||||
base_query
|
base_query
|
||||||
.order_by(Tag.name)
|
.order_by(Tag.name)
|
||||||
@@ -1285,6 +1291,72 @@ def regenerate_missing_thumbnails_celery():
|
|||||||
return jsonify({'ok': False, 'error': str(e)}), 500
|
return jsonify({'ok': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@main.route('/api/reapply-artist-tags', methods=['POST'])
|
||||||
|
def reapply_artist_tags_from_paths():
|
||||||
|
"""
|
||||||
|
Re-apply artist tags to all images based on their file paths.
|
||||||
|
Artist is extracted from the first directory component after /images/
|
||||||
|
(e.g., /images/artist_name/file.jpg -> artist:artist_name)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
images = ImageRecord.query.all()
|
||||||
|
updated = 0
|
||||||
|
skipped = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for img in images:
|
||||||
|
if not img.filepath:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract artist from path: /images/{artist}/...
|
||||||
|
# Handle both /images/artist/file.jpg and /images/artist/subdir/file.jpg
|
||||||
|
path_parts = img.filepath.split('/')
|
||||||
|
try:
|
||||||
|
# Find 'images' in path and get the next component
|
||||||
|
images_idx = path_parts.index('images')
|
||||||
|
if images_idx + 1 < len(path_parts):
|
||||||
|
artist_name = path_parts[images_idx + 1]
|
||||||
|
else:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
except ValueError:
|
||||||
|
# 'images' not in path, try alternative format
|
||||||
|
# Maybe it's a Windows path or different structure
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not artist_name or artist_name == '':
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create or get the artist tag
|
||||||
|
tag_name = f"artist:{artist_name}"
|
||||||
|
tag = Tag.query.filter_by(name=tag_name).first()
|
||||||
|
if not tag:
|
||||||
|
tag = Tag(name=tag_name, kind='artist')
|
||||||
|
db.session.add(tag)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# Add tag if not already present
|
||||||
|
if tag not in img.tags:
|
||||||
|
img.tags.append(tag)
|
||||||
|
updated += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'ok': True,
|
||||||
|
'updated': updated,
|
||||||
|
'skipped': skipped,
|
||||||
|
'total': len(images),
|
||||||
|
'errors': errors if errors else None
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'ok': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@main.route('/api/celery/status')
|
@main.route('/api/celery/status')
|
||||||
def celery_cluster_status():
|
def celery_cluster_status():
|
||||||
"""
|
"""
|
||||||
@@ -1865,3 +1937,57 @@ def get_image_series_info(image_id):
|
|||||||
'series_tag_id': sp.series_tag_id,
|
'series_tag_id': sp.series_tag_id,
|
||||||
'series_name': sp.series_tag.name
|
'series_name': sp.series_tag.name
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@main.post('/api/image/<int:image_id>/add-to-series')
|
||||||
|
def add_image_to_series(image_id):
|
||||||
|
"""Add a single image to a series with a specific page number."""
|
||||||
|
from app.models import SeriesPage, Tag
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
series_tag_id = data.get('series_tag_id')
|
||||||
|
page_number = data.get('page_number')
|
||||||
|
|
||||||
|
if not series_tag_id:
|
||||||
|
return jsonify(ok=False, error="series_tag_id required"), 400
|
||||||
|
if not page_number:
|
||||||
|
return jsonify(ok=False, error="page_number required"), 400
|
||||||
|
|
||||||
|
# Verify the series tag exists and is a series
|
||||||
|
series_tag = Tag.query.get(series_tag_id)
|
||||||
|
if not series_tag:
|
||||||
|
return jsonify(ok=False, error="Series not found"), 404
|
||||||
|
if series_tag.kind != 'series':
|
||||||
|
return jsonify(ok=False, error="Tag is not a series"), 400
|
||||||
|
|
||||||
|
# Check if image is already in this series
|
||||||
|
existing = SeriesPage.query.filter_by(
|
||||||
|
series_tag_id=series_tag_id,
|
||||||
|
image_id=image_id
|
||||||
|
).first()
|
||||||
|
if existing:
|
||||||
|
return jsonify(ok=False, error="Image already in this series"), 409
|
||||||
|
|
||||||
|
# Check if page number is taken
|
||||||
|
page_taken = SeriesPage.query.filter_by(
|
||||||
|
series_tag_id=series_tag_id,
|
||||||
|
page_number=page_number
|
||||||
|
).first()
|
||||||
|
if page_taken:
|
||||||
|
return jsonify(ok=False, error=f"Page {page_number} already exists in this series"), 409
|
||||||
|
|
||||||
|
# Create the series page
|
||||||
|
sp = SeriesPage(
|
||||||
|
series_tag_id=series_tag_id,
|
||||||
|
image_id=image_id,
|
||||||
|
page_number=page_number
|
||||||
|
)
|
||||||
|
db.session.add(sp)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify(ok=True, series_page={
|
||||||
|
'id': sp.id,
|
||||||
|
'page_number': sp.page_number,
|
||||||
|
'series_tag_id': sp.series_tag_id,
|
||||||
|
'series_name': series_tag.name
|
||||||
|
})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const tagInput = tagForm ? tagForm.querySelector('input[name="name"]') : null;
|
const tagInput = tagForm ? tagForm.querySelector('input[name="name"]') : null;
|
||||||
const tagAutocomplete = document.getElementById('tagAutocomplete');
|
const tagAutocomplete = document.getElementById('tagAutocomplete');
|
||||||
|
|
||||||
// Series info elements
|
// Series info elements (when image IS in a series)
|
||||||
const seriesInfoEl = document.getElementById('modalSeriesInfo');
|
const seriesInfoEl = document.getElementById('modalSeriesInfo');
|
||||||
const seriesNameEl = document.getElementById('modalSeriesName');
|
const seriesNameEl = document.getElementById('modalSeriesName');
|
||||||
const pageNumberInput = document.getElementById('modalPageNumber');
|
const pageNumberInput = document.getElementById('modalPageNumber');
|
||||||
@@ -22,6 +22,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const pageUpdateStatus = document.getElementById('pageUpdateStatus');
|
const pageUpdateStatus = document.getElementById('pageUpdateStatus');
|
||||||
let currentSeriesPageId = null;
|
let currentSeriesPageId = null;
|
||||||
|
|
||||||
|
// Add to series elements (when image is NOT in a series)
|
||||||
|
const addToSeriesEl = document.getElementById('modalAddToSeries');
|
||||||
|
const seriesSelect = document.getElementById('seriesSelect');
|
||||||
|
const addSeriesPageNumber = document.getElementById('addSeriesPageNumber');
|
||||||
|
const addToSeriesBtn = document.getElementById('addToSeriesBtn');
|
||||||
|
const addToSeriesStatus = document.getElementById('addToSeriesStatus');
|
||||||
|
let seriesTagsLoaded = false;
|
||||||
|
|
||||||
// Autocomplete state
|
// Autocomplete state
|
||||||
let autocompleteItems = [];
|
let autocompleteItems = [];
|
||||||
let autocompleteSelectedIndex = -1;
|
let autocompleteSelectedIndex = -1;
|
||||||
@@ -106,6 +114,82 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (pageUpdateStatus) pageUpdateStatus.textContent = '';
|
if (pageUpdateStatus) pageUpdateStatus.textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hideAddToSeries() {
|
||||||
|
if (addToSeriesEl) addToSeriesEl.style.display = 'none';
|
||||||
|
if (addToSeriesStatus) addToSeriesStatus.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSeriesTags() {
|
||||||
|
if (!seriesSelect || seriesTagsLoaded) return;
|
||||||
|
try {
|
||||||
|
// Fetch all series tags using the kind filter
|
||||||
|
const r = await fetch('/api/tags/search?kind=series&limit=100');
|
||||||
|
const j = await r.json();
|
||||||
|
const seriesTags = j.tags || [];
|
||||||
|
|
||||||
|
// Clear existing options except the first placeholder
|
||||||
|
seriesSelect.innerHTML = '<option value="">Select series...</option>';
|
||||||
|
|
||||||
|
seriesTags.forEach(tag => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = tag.id;
|
||||||
|
// Display name without prefix
|
||||||
|
opt.textContent = tag.name.includes(':') ? tag.name.split(':', 2)[1] : tag.name;
|
||||||
|
seriesSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
seriesTagsLoaded = true;
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAddToSeries() {
|
||||||
|
if (!addToSeriesEl) return;
|
||||||
|
loadSeriesTags();
|
||||||
|
if (seriesSelect) seriesSelect.value = '';
|
||||||
|
if (addSeriesPageNumber) addSeriesPageNumber.value = '1';
|
||||||
|
if (addToSeriesBtn) addToSeriesBtn.disabled = true;
|
||||||
|
if (addToSeriesStatus) addToSeriesStatus.textContent = '';
|
||||||
|
addToSeriesEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddToSeries() {
|
||||||
|
const imageId = getEditorImageId();
|
||||||
|
const seriesTagId = seriesSelect?.value;
|
||||||
|
const pageNumber = parseInt(addSeriesPageNumber?.value);
|
||||||
|
|
||||||
|
if (!imageId || !seriesTagId || isNaN(pageNumber) || pageNumber < 1) return;
|
||||||
|
|
||||||
|
if (addToSeriesBtn) addToSeriesBtn.disabled = true;
|
||||||
|
if (addToSeriesStatus) addToSeriesStatus.textContent = 'Adding...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/image/${imageId}/add-to-series`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ series_tag_id: parseInt(seriesTagId), page_number: pageNumber })
|
||||||
|
});
|
||||||
|
const j = await r.json();
|
||||||
|
|
||||||
|
if (j.ok) {
|
||||||
|
if (addToSeriesStatus) {
|
||||||
|
addToSeriesStatus.textContent = '✓ Added';
|
||||||
|
setTimeout(() => { addToSeriesStatus.textContent = ''; }, 2000);
|
||||||
|
}
|
||||||
|
// Switch to showing the series info instead of add-to-series
|
||||||
|
hideAddToSeries();
|
||||||
|
showSeriesInfo(j.series_page);
|
||||||
|
} else {
|
||||||
|
if (addToSeriesStatus) addToSeriesStatus.textContent = j.error || 'Error';
|
||||||
|
if (addToSeriesBtn) addToSeriesBtn.disabled = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (addToSeriesStatus) addToSeriesStatus.textContent = 'Error';
|
||||||
|
if (addToSeriesBtn) addToSeriesBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showSeriesInfo(seriesPage) {
|
function showSeriesInfo(seriesPage) {
|
||||||
if (!seriesInfoEl || !seriesPage) return;
|
if (!seriesInfoEl || !seriesPage) return;
|
||||||
currentSeriesPageId = seriesPage.id;
|
currentSeriesPageId = seriesPage.id;
|
||||||
@@ -130,12 +214,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
async function loadSeriesInfo(imageId) {
|
async function loadSeriesInfo(imageId) {
|
||||||
hideSeriesInfo();
|
hideSeriesInfo();
|
||||||
|
hideAddToSeries();
|
||||||
if (!imageId) return;
|
if (!imageId) return;
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/api/image/${imageId}/series-info`);
|
const r = await fetch(`/api/image/${imageId}/series-info`);
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (j.ok && j.in_series) {
|
if (j.ok && j.in_series) {
|
||||||
showSeriesInfo(j.series_page);
|
showSeriesInfo(j.series_page);
|
||||||
|
} else {
|
||||||
|
// Show add-to-series option when not in a series
|
||||||
|
showAddToSeries();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
@@ -190,6 +278,31 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable/disable add button based on series selection
|
||||||
|
if (seriesSelect) {
|
||||||
|
seriesSelect.addEventListener('change', () => {
|
||||||
|
if (addToSeriesBtn) {
|
||||||
|
addToSeriesBtn.disabled = !seriesSelect.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to series button click
|
||||||
|
if (addToSeriesBtn) {
|
||||||
|
addToSeriesBtn.addEventListener('click', handleAddToSeries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Enter key in add-to-series page number input
|
||||||
|
if (addSeriesPageNumber) {
|
||||||
|
addSeriesPageNumber.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && seriesSelect?.value) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddToSeries();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function addTag(imageId, name) {
|
async function addTag(imageId, name) {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('name', name);
|
fd.append('name', name);
|
||||||
@@ -429,6 +542,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
setEditorImageId('');
|
setEditorImageId('');
|
||||||
renderTags([]);
|
renderTags([]);
|
||||||
hideSeriesInfo();
|
hideSeriesInfo();
|
||||||
|
hideAddToSeries();
|
||||||
history.replaceState({}, '', window.location.pathname + window.location.search);
|
history.replaceState({}, '', window.location.pathname + window.location.search);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -642,6 +642,37 @@ header {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add to series section in modal */
|
||||||
|
.modal-add-to-series {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.add-series-header {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.add-series-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.series-select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.series-select option {
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.tag-editor .tags {
|
.tag-editor .tags {
|
||||||
margin: 0 0 0.5rem 0;
|
margin: 0 0 0.5rem 0;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="modalTagEditor" class="tag-editor" data-image-id="">
|
<div id="modalTagEditor" class="tag-editor" data-image-id="">
|
||||||
|
<!-- Shown when image IS in a series -->
|
||||||
<div id="modalSeriesInfo" class="modal-series-info" style="display: none;">
|
<div id="modalSeriesInfo" class="modal-series-info" style="display: none;">
|
||||||
<div class="series-info-row">
|
<div class="series-info-row">
|
||||||
<span class="series-info-label">📚 Page</span>
|
<span class="series-info-label">📚 Page</span>
|
||||||
@@ -22,6 +23,20 @@
|
|||||||
<span id="pageUpdateStatus" class="page-update-status"></span>
|
<span id="pageUpdateStatus" class="page-update-status"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Shown when image is NOT in a series -->
|
||||||
|
<div id="modalAddToSeries" class="modal-add-to-series" style="display: none;">
|
||||||
|
<div class="add-series-header">
|
||||||
|
<span class="series-info-label">📚 Add to Series</span>
|
||||||
|
</div>
|
||||||
|
<div class="add-series-controls">
|
||||||
|
<select id="seriesSelect" class="series-select">
|
||||||
|
<option value="">Select series...</option>
|
||||||
|
</select>
|
||||||
|
<input type="number" id="addSeriesPageNumber" min="1" value="1" class="series-page-input" placeholder="#">
|
||||||
|
<button id="addToSeriesBtn" class="btn-small" disabled>Add</button>
|
||||||
|
</div>
|
||||||
|
<span id="addToSeriesStatus" class="page-update-status"></span>
|
||||||
|
</div>
|
||||||
<div id="modalTagList" class="tags"></div>
|
<div id="modalTagList" class="tags"></div>
|
||||||
<form id="modalTagForm" class="tag-form" autocomplete="off">
|
<form id="modalTagForm" class="tag-form" autocomplete="off">
|
||||||
<div class="tag-form-wrapper">
|
<div class="tag-form-wrapper">
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<button id="retryFailedBtn" class="btn warning-btn">Retry Failed Tasks</button>
|
<button id="retryFailedBtn" class="btn warning-btn">Retry Failed Tasks</button>
|
||||||
<button id="clearCompletedBtn" class="btn secondary-btn">Clear Completed</button>
|
<button id="clearCompletedBtn" class="btn secondary-btn">Clear Completed</button>
|
||||||
<button id="regenMissingThumbsBtn" class="btn secondary-btn">Regenerate Missing Thumbnails</button>
|
<button id="regenMissingThumbsBtn" class="btn secondary-btn">Regenerate Missing Thumbnails</button>
|
||||||
|
<button id="reapplyArtistTagsBtn" class="btn secondary-btn">Reapply Artist Tags from Paths</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="celeryWorkerInfo" style="margin-top: 1rem;">
|
<div id="celeryWorkerInfo" style="margin-top: 1rem;">
|
||||||
@@ -733,6 +734,34 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reapply Artist Tags from Paths
|
||||||
|
document.getElementById('reapplyArtistTagsBtn').addEventListener('click', async () => {
|
||||||
|
const btn = document.getElementById('reapplyArtistTagsBtn');
|
||||||
|
|
||||||
|
if (!confirm('This will re-apply artist tags to all images based on their file paths. Continue?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Processing...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/reapply-artist-tags', { method: 'POST' });
|
||||||
|
const data = await r.json();
|
||||||
|
|
||||||
|
if (data.ok) {
|
||||||
|
alert(`Artist tags reapplied!\n\nUpdated: ${data.updated}\nSkipped: ${data.skipped}\nTotal images: ${data.total}`);
|
||||||
|
} else {
|
||||||
|
alert('Failed: ' + (data.error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Failed: ' + e.message);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Reapply Artist Tags from Paths';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Load import settings
|
// Load import settings
|
||||||
fetch('/api/import-settings')
|
fetch('/api/import-settings')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
|
|||||||
Reference in New Issue
Block a user