implement post metadata import and views. implemented series paging and reader view.

This commit is contained in:
Bryan Van Deusen
2026-01-22 22:19:11 -05:00
parent f41acb40f6
commit a05aee5a07
16 changed files with 2298 additions and 8 deletions
+356 -3
View File
@@ -84,6 +84,8 @@ def gallery():
Server renders initial batch, then JS takes over for infinite scroll.
Optional tag filter via ?tag=artist:NAME or any tag name.
"""
from app.models import PostMetadata
initial_limit = 30
tag_name = request.args.get('tag')
@@ -91,8 +93,11 @@ def gallery():
q = ImageRecord.query.options(joinedload(ImageRecord.tags))
# Optional tag filter
active_tag_obj = None
if tag_name:
q = q.join(ImageRecord.tags).filter(Tag.name == tag_name)
active_tag_obj = Tag.query.filter_by(name=tag_name).first()
if active_tag_obj:
q = q.join(ImageRecord.tags).filter(Tag.name == tag_name)
# Fetch initial batch ordered by date descending
images = q.order_by(
@@ -115,12 +120,42 @@ def gallery():
if last_dt:
initial_cursor = last_dt.isoformat()
# Fetch post metadata if filtering by a post tag
post_metadata = None
if active_tag_obj and active_tag_obj.kind == 'post':
pm = PostMetadata.query.filter_by(tag_id=active_tag_obj.id).first()
if pm:
post_metadata = {
'title': pm.title,
'description': pm.description,
'source_url': pm.source_url,
'platform': pm.platform,
'artist': pm.artist,
'post_id': pm.post_id,
'attachment_count': pm.attachment_count,
'published_at': pm.published_at,
}
# Fetch series info if filtering by a series tag
from app.models import SeriesPage
series_info = None
if active_tag_obj and active_tag_obj.kind == 'series':
page_count = SeriesPage.query.filter_by(series_tag_id=active_tag_obj.id).count()
series_info = {
'tag_id': active_tag_obj.id,
'name': active_tag_obj.name.split(':', 1)[1] if ':' in active_tag_obj.name else active_tag_obj.name,
'page_count': page_count,
}
return render_template(
'gallery.html',
grouped_images=grouped_images,
active_tag=tag_name,
active_tag_obj=active_tag_obj,
initial_cursor=initial_cursor,
has_more=has_more
has_more=has_more,
post_metadata=post_metadata,
series_info=series_info
)
@@ -605,6 +640,73 @@ def get_tag(tag_id):
})
@main.get("/api/post-metadata/<int:tag_id>")
def get_post_metadata(tag_id):
"""
Get post metadata for a post tag.
Returns title, description, source URL, etc. for display in the gallery.
"""
from app.models import PostMetadata
tag = Tag.query.get_or_404(tag_id)
# Only post tags have metadata
if tag.kind != 'post':
return jsonify(ok=False, error="Not a post tag"), 400
pm = PostMetadata.query.filter_by(tag_id=tag_id).first()
if not pm:
return jsonify(ok=True, metadata=None)
return jsonify(ok=True, metadata={
"id": pm.id,
"tag_id": pm.tag_id,
"tag_name": tag.name,
"platform": pm.platform,
"post_id": pm.post_id,
"artist": pm.artist,
"title": pm.title,
"description": pm.description,
"source_url": pm.source_url,
"attachment_count": pm.attachment_count,
"published_at": pm.published_at.isoformat() if pm.published_at else None,
})
@main.get("/api/post-metadata/by-tag-name/<path:tag_name>")
def get_post_metadata_by_name(tag_name):
"""
Get post metadata by tag name (e.g., post:patreon:artist:12345).
Useful when filtering gallery by tag name.
"""
from app.models import PostMetadata
tag = Tag.query.filter_by(name=tag_name).first()
if not tag:
return jsonify(ok=True, metadata=None)
if tag.kind != 'post':
return jsonify(ok=False, error="Not a post tag"), 400
pm = PostMetadata.query.filter_by(tag_id=tag.id).first()
if not pm:
return jsonify(ok=True, metadata=None)
return jsonify(ok=True, metadata={
"id": pm.id,
"tag_id": pm.tag_id,
"tag_name": tag.name,
"platform": pm.platform,
"post_id": pm.post_id,
"artist": pm.artist,
"title": pm.title,
"description": pm.description,
"source_url": pm.source_url,
"attachment_count": pm.attachment_count,
"published_at": pm.published_at.isoformat() if pm.published_at else None,
})
@main.post("/api/tag/<int:tag_id>/update")
def update_tag(tag_id):
"""
@@ -627,7 +729,7 @@ def update_tag(tag_id):
return jsonify(ok=False, error="Name is required"), 400
# Build the full tag name based on kind
prefixed_kinds = ("artist", "archive", "character", "series", "rating")
prefixed_kinds = ("artist", "archive", "character", "series", "fandom", "rating")
if new_kind in prefixed_kinds:
# Remove any existing prefix from name if present
if ":" in new_name:
@@ -1512,3 +1614,254 @@ def bulk_remove_tag():
'ok': True,
'removed_count': removed_count
})
# =============================================
# Series / Reader API
# =============================================
@main.route('/read/<int:tag_id>')
def read_series(tag_id):
"""
Reader view for a series - displays images in page order.
Supports jump-to-page via ?page=N parameter.
"""
from app.models import SeriesPage
tag = Tag.query.get_or_404(tag_id)
if tag.kind != 'series':
return "Not a series tag", 400
# Get all pages in order
pages = SeriesPage.query.filter_by(series_tag_id=tag_id).order_by(
SeriesPage.page_number
).all()
# Get starting page from query param
start_page = request.args.get('page', 1, type=int)
# Get series name (strip prefix if present)
series_name = tag.name.split(':', 1)[1] if ':' in tag.name else tag.name
return render_template(
'reader.html',
tag=tag,
series_name=series_name,
pages=pages,
start_page=start_page,
total_pages=len(pages)
)
@main.get('/api/series/<int:tag_id>/pages')
def get_series_pages(tag_id):
"""Get all pages in a series with their images."""
from app.models import SeriesPage
tag = Tag.query.get_or_404(tag_id)
if tag.kind != 'series':
return jsonify(ok=False, error="Not a series tag"), 400
pages = SeriesPage.query.filter_by(series_tag_id=tag_id).order_by(
SeriesPage.page_number
).all()
return jsonify(ok=True, pages=[
{
'id': p.id,
'page_number': p.page_number,
'image_id': p.image_id,
'image': {
'id': p.image.id,
'filename': p.image.filename,
'filepath': p.image.filepath,
'thumb_path': p.image.thumb_path,
'width': p.image.width,
'height': p.image.height,
}
}
for p in pages
], total_pages=len(pages))
@main.post('/api/series/<int:tag_id>/pages/add')
def add_series_page(tag_id):
"""
Add an image to a series with a specific page number.
Expects form data: image_id, page_number
"""
from app.models import SeriesPage
tag = Tag.query.get_or_404(tag_id)
if tag.kind != 'series':
return jsonify(ok=False, error="Not a series tag"), 400
image_id = request.form.get('image_id', type=int)
page_number = request.form.get('page_number', type=int)
if not image_id or page_number is None:
return jsonify(ok=False, error="image_id and page_number required"), 400
# Check if image exists
image = ImageRecord.query.get(image_id)
if not image:
return jsonify(ok=False, error="Image not found"), 404
# Check if image is already in a series
existing = SeriesPage.query.filter_by(image_id=image_id).first()
if existing:
return jsonify(ok=False, error="Image is already in a series",
existing_series_id=existing.series_tag_id), 409
# Check if page number is already taken
page_exists = SeriesPage.query.filter_by(
series_tag_id=tag_id, page_number=page_number
).first()
if page_exists:
return jsonify(ok=False, error=f"Page {page_number} already exists"), 409
# Create the series page
sp = SeriesPage(
series_tag_id=tag_id,
image_id=image_id,
page_number=page_number
)
db.session.add(sp)
# Also ensure the image has the series tag
if tag not in image.tags:
image.tags.append(tag)
db.session.commit()
return jsonify(ok=True, page={
'id': sp.id,
'page_number': sp.page_number,
'image_id': sp.image_id
})
@main.post('/api/series/<int:tag_id>/pages/bulk-add')
def bulk_add_series_pages(tag_id):
"""
Add multiple images to a series with sequential page numbers.
Expects JSON: { image_ids: [id1, id2, ...], start_page: N }
Images are added in order starting from start_page.
"""
from app.models import SeriesPage
tag = Tag.query.get_or_404(tag_id)
if tag.kind != 'series':
return jsonify(ok=False, error="Not a series tag"), 400
data = request.get_json() or {}
image_ids = data.get('image_ids', [])
start_page = data.get('start_page', 1)
if not image_ids:
return jsonify(ok=False, error="No image IDs provided"), 400
added = []
skipped = []
current_page = start_page
for image_id in image_ids:
image = ImageRecord.query.get(image_id)
if not image:
skipped.append({'image_id': image_id, 'reason': 'not found'})
continue
# Check if image is already in a series
existing = SeriesPage.query.filter_by(image_id=image_id).first()
if existing:
skipped.append({'image_id': image_id, 'reason': 'already in series'})
continue
# Find next available page number
while SeriesPage.query.filter_by(
series_tag_id=tag_id, page_number=current_page
).first():
current_page += 1
sp = SeriesPage(
series_tag_id=tag_id,
image_id=image_id,
page_number=current_page
)
db.session.add(sp)
# Ensure image has the series tag
if tag not in image.tags:
image.tags.append(tag)
added.append({'image_id': image_id, 'page_number': current_page})
current_page += 1
db.session.commit()
return jsonify(ok=True, added=added, skipped=skipped)
@main.post('/api/series/page/<int:page_id>/update')
def update_series_page(page_id):
"""Update a series page's page number."""
from app.models import SeriesPage
sp = SeriesPage.query.get_or_404(page_id)
new_page_number = request.form.get('page_number', type=int)
if new_page_number is None:
return jsonify(ok=False, error="page_number required"), 400
# Check if the new page number is taken
existing = SeriesPage.query.filter(
SeriesPage.series_tag_id == sp.series_tag_id,
SeriesPage.page_number == new_page_number,
SeriesPage.id != page_id
).first()
if existing:
return jsonify(ok=False, error=f"Page {new_page_number} already exists"), 409
sp.page_number = new_page_number
db.session.commit()
return jsonify(ok=True, page={
'id': sp.id,
'page_number': sp.page_number,
'image_id': sp.image_id
})
@main.post('/api/series/page/<int:page_id>/delete')
def delete_series_page(page_id):
"""Remove an image from a series (doesn't delete the image)."""
from app.models import SeriesPage
sp = SeriesPage.query.get_or_404(page_id)
db.session.delete(sp)
db.session.commit()
return jsonify(ok=True)
@main.get('/api/image/<int:image_id>/series-info')
def get_image_series_info(image_id):
"""Check if an image is part of a series and return its page info."""
from app.models import SeriesPage
sp = SeriesPage.query.filter_by(image_id=image_id).first()
if not sp:
return jsonify(ok=True, in_series=False)
return jsonify(ok=True, in_series=True, series_page={
'id': sp.id,
'page_number': sp.page_number,
'series_tag_id': sp.series_tag_id,
'series_name': sp.series_tag.name
})