implement post metadata import and views. implemented series paging and reader view.
This commit is contained in:
+356
-3
@@ -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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user