From 3153455355339d909f5a63d7fc190aa323feec48 Mon Sep 17 00:00:00 2001 From: mi Date: Sat, 15 Nov 2025 20:40:18 +1000 Subject: [PATCH] :memo: consolidate multiple list endpoints for comics --- app.py | 40 +++--- static/js/archive-lazy-load.js | 2 +- static/openapi.yaml | 215 ++++++++++++++++++++++++++++----- 3 files changed, 209 insertions(+), 48 deletions(-) diff --git a/app.py b/app.py index 9f96d91..858535c 100644 --- a/app.py +++ b/app.py @@ -318,24 +318,19 @@ def terms(): @app.route('/api/comics') def api_comics(): - """API endpoint - returns all comics as JSON""" - return jsonify([enrich_comic(comic) for comic in COMICS]) + """API endpoint - returns all comics as JSON (optionally paginated with sections)""" + # Check for pagination parameters + page = request.args.get('page', type=int) + per_page = request.args.get('per_page', type=int) + group_by_section = request.args.get('group_by_section', 'false').lower() in ('true', '1', 'yes') + # If no pagination requested, return simple array (backward compatible) + if page is None and per_page is None and not group_by_section: + return jsonify([enrich_comic(comic) for comic in COMICS]) -@app.route('/api/comics/') -def api_comic(comic_id): - """API endpoint - returns a specific comic as JSON""" - comic = get_comic_by_number(comic_id) - if not comic: - return jsonify({'error': 'Comic not found'}), 404 - return jsonify(comic) - - -@app.route('/api/archive') -def api_archive(): - """API endpoint - returns paginated archive data""" - page = request.args.get('page', 1, type=int) - per_page = request.args.get('per_page', 24, type=int) + # Pagination requested - return paginated response + page = page or 1 + per_page = per_page or 24 # Limit per_page to reasonable values per_page = min(max(per_page, 1), 100) @@ -343,8 +338,8 @@ def api_archive(): # Reverse order to show newest first all_comics = [enrich_comic(comic) for comic in reversed(COMICS)] - # Group by section if enabled - sections = group_comics_by_section(all_comics) + # Group by section if enabled globally or requested via parameter + sections = group_comics_by_section(all_comics) if (SECTIONS_ENABLED or group_by_section) else [(None, all_comics)] # Calculate pagination total_comics = len(all_comics) @@ -388,6 +383,15 @@ def api_archive(): }) +@app.route('/api/comics/') +def api_comic(comic_id): + """API endpoint - returns a specific comic as JSON""" + comic = get_comic_by_number(comic_id) + if not comic: + return jsonify({'error': 'Comic not found'}), 404 + return jsonify(comic) + + @app.route('/sitemap.xml') def sitemap(): """Serve the static sitemap.xml file""" diff --git a/static/js/archive-lazy-load.js b/static/js/archive-lazy-load.js index 7d3420c..c66e1e1 100644 --- a/static/js/archive-lazy-load.js +++ b/static/js/archive-lazy-load.js @@ -41,7 +41,7 @@ try { currentPage++; - const response = await fetch(`/api/archive?page=${currentPage}&per_page=${perPage}`); + const response = await fetch(`/api/comics?page=${currentPage}&per_page=${perPage}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); diff --git a/static/openapi.yaml b/static/openapi.yaml index 84622fa..6e16515 100644 --- a/static/openapi.yaml +++ b/static/openapi.yaml @@ -16,39 +16,107 @@ paths: /api/comics: get: summary: Get all comics - description: Returns a list of all comics with enriched metadata including formatted dates and author notes + description: | + Returns all comics with enriched metadata. Supports optional pagination and section grouping. + + **Without pagination parameters:** Returns a simple array of all comics (newest first when using pagination, original order otherwise). + + **With pagination parameters:** Returns paginated response with section grouping (if enabled globally or via `group_by_section` parameter). operationId: getAllComics tags: - Comics + parameters: + - name: page + in: query + description: Page number for pagination (1-indexed). When provided, triggers paginated response format. + required: false + schema: + type: integer + minimum: 1 + default: 1 + example: 1 + - name: per_page + in: query + description: Number of comics per page (max 100). When provided, triggers paginated response format. + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 24 + example: 24 + - name: group_by_section + in: query + description: Force section grouping in response (even when SECTIONS_ENABLED is false). When true, triggers paginated response format. + required: false + schema: + type: boolean + default: false + example: false responses: '200': - description: Successful response with array of comics + description: Successful response content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/Comic' - example: - - number: 1 - title: "First Comic" - filename: "comic-001.jpg" - mobile_filename: "comic-001-mobile.jpg" - date: "2025-01-01" - alt_text: "The very first comic" - author_note: "This is where your comic journey begins!" - full_width: true - plain: true - formatted_date: "Wednesday, January 1, 2025" - author_note_is_html: false - - number: 2 - filename: "comic-002.jpg" - date: "2025-01-08" - alt_text: "The second comic" - full_width: true - plain: true - formatted_date: "Wednesday, January 8, 2025" - author_note_is_html: false + oneOf: + - type: array + description: Simple array response (when no pagination parameters provided) + items: + $ref: '#/components/schemas/Comic' + - $ref: '#/components/schemas/PaginatedComicsResponse' + examples: + simpleArray: + summary: Simple array response (default) + value: + - number: 1 + title: "First Comic" + filename: "comic-001.jpg" + mobile_filename: "comic-001-mobile.jpg" + date: "2025-01-01" + alt_text: "The very first comic" + author_note: "This is where your comic journey begins!" + full_width: true + plain: true + formatted_date: "Wednesday, January 1, 2025" + author_note_is_html: false + - number: 2 + filename: "comic-002.jpg" + date: "2025-01-08" + alt_text: "The second comic" + full_width: true + plain: true + formatted_date: "Wednesday, January 8, 2025" + author_note_is_html: false + paginatedResponse: + summary: Paginated response (when using page/per_page parameters) + value: + sections: + - section_title: "Chapter 1" + comics: + - number: 2 + filename: "comic-002.jpg" + date: "2025-01-08" + alt_text: "The second comic" + full_width: true + plain: true + formatted_date: "Wednesday, January 8, 2025" + author_note_is_html: false + - section_title: null + comics: + - number: 1 + title: "First Comic" + filename: "comic-001.jpg" + date: "2025-01-01" + alt_text: "The very first comic" + full_width: true + plain: true + formatted_date: "Wednesday, January 1, 2025" + author_note_is_html: false + page: 1 + per_page: 24 + total_comics: 2 + has_more: false /api/comics/{comic_id}: get: @@ -107,6 +175,9 @@ components: - plain - formatted_date - author_note_is_html + - filenames + - alt_texts + - is_multi_image properties: number: type: integer @@ -118,9 +189,20 @@ components: description: Comic title (optional, defaults to "#X" if not provided) example: "First Comic" filename: - type: string - description: Image filename in static/images/comics/ + oneOf: + - type: string + description: Single image filename in static/images/comics/ + - type: array + description: Multiple image filenames for webtoon-style comics + items: + type: string example: "comic-001.jpg" + filenames: + type: array + description: Normalized array of image filenames (computed field, always an array even for single images) + items: + type: string + example: ["comic-001.jpg"] mobile_filename: type: string description: Optional mobile version of the comic image @@ -131,13 +213,36 @@ components: description: Publication date in YYYY-MM-DD format example: "2025-01-01" alt_text: - type: string - description: Accessibility text for the comic image + oneOf: + - type: string + description: Accessibility text for single image or shared text for all images + - type: array + description: Individual accessibility text for each image in multi-image comics + items: + type: string example: "The very first comic" + alt_texts: + type: array + description: Normalized array of alt texts matching filenames (computed field) + items: + type: string + example: ["The very first comic"] + is_multi_image: + type: boolean + description: Indicates if this is a multi-image comic (computed field) + example: false author_note: type: string description: Author's note about the comic (plain text or HTML from markdown) example: "This is where your comic journey begins!" + author_note_md: + type: string + description: Filename or path to markdown file for author note + example: "2025-01-01.md" + section: + type: string + description: Section/chapter title (appears on archive page when SECTIONS_ENABLED is true) + example: "Chapter 1: Origins" full_width: type: boolean description: Whether the comic should display in full-width mode (computed from global default and per-comic override) @@ -155,6 +260,58 @@ components: description: Indicates whether author_note contains HTML (from markdown) or plain text (computed field) example: false + ComicSection: + type: object + required: + - section_title + - comics + properties: + section_title: + type: string + nullable: true + description: Section/chapter title (null for comics without a section) + example: "Chapter 1" + comics: + type: array + description: Comics in this section + items: + $ref: '#/components/schemas/Comic' + + PaginatedComicsResponse: + type: object + required: + - sections + - page + - per_page + - total_comics + - has_more + properties: + sections: + type: array + description: Comics grouped by section + items: + $ref: '#/components/schemas/ComicSection' + page: + type: integer + description: Current page number + minimum: 1 + example: 1 + per_page: + type: integer + description: Number of comics per page + minimum: 1 + maximum: 100 + example: 24 + total_comics: + type: integer + description: Total number of comics across all pages + minimum: 0 + example: 100 + has_more: + type: boolean + description: Whether there are more pages available + example: true + Error: type: object required: