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)
|
||||
- limit: max results (default 10)
|
||||
- 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()
|
||||
limit = request.args.get('limit', 10, type=int)
|
||||
exclude_kind = request.args.get('exclude_kind', '').strip()
|
||||
exclude_kinds = [k.strip() for k in exclude_kind.split(',') if k.strip()]
|
||||
include_kind = request.args.get('kind', '').strip()
|
||||
|
||||
if not query:
|
||||
# Return most-used tags when no query
|
||||
@@ -552,6 +554,8 @@ def search_tags():
|
||||
)
|
||||
if 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 = (
|
||||
base_query
|
||||
.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}%'))
|
||||
if 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 = (
|
||||
base_query
|
||||
.order_by(Tag.name)
|
||||
@@ -1285,6 +1291,72 @@ def regenerate_missing_thumbnails_celery():
|
||||
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')
|
||||
def celery_cluster_status():
|
||||
"""
|
||||
@@ -1865,3 +1937,57 @@ def get_image_series_info(image_id):
|
||||
'series_tag_id': sp.series_tag_id,
|
||||
'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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user