# ImageRepo - Codebase Summary A Flask-based image repository with Celery task processing for importing, organizing, and managing images/videos with tagging, duplicate detection, and Gallery-DL metadata integration. --- ## Architecture Overview ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Flask Web │ │ Celery Worker │ │ Celery Beat │ │ (port 5000) │ │ (task queues) │ │ (scheduler) │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ │ └───────────────────────┼───────────────────────┘ │ ┌──────────────────┼──────────────────┐ │ │ │ ┌─────▼─────┐ ┌──────▼──────┐ ┌─────▼─────┐ │ PostgreSQL│ │ Redis │ │ /images │ │ (database)│ │ (broker) │ │ /import │ └───────────┘ └─────────────┘ └───────────┘ ``` --- ## File Structure ### Core Application Files | File | Purpose | |------|---------| | `run.py` | Entry point - creates Flask app via `create_app()` | | `config.py` | Configuration class with DB, Redis, Celery settings | | `docker-compose.yml` | Container orchestration for all services | ### Application Package (`app/`) | File | Purpose | |------|---------| | `app/__init__.py:56-86` | Flask app factory `create_app()`, CLI commands, initializes extensions | | `app/main.py` | Blueprint with all HTTP routes and API endpoints | | `app/models.py` | SQLAlchemy models for database schema | | `app/celery_app.py` | Celery configuration and task routing | --- ## Database Models (`app/models.py`) ### ImageRecord (lines 19-40) Primary model for images/videos. ``` Fields: id, filename, filepath, thumb_path, hash (SHA256), perceptual_hash (256-bit), file_size, width, height, format, camera_model, taken_at, imported_at, is_thumbnail Relationships: tags (many-to-many via image_tags) ``` ### Tag (lines 42-51) Tagging system for organization. ``` Fields: id, name (unique), kind (artist|archive|user|source|post|character|series|rating) Relationships: images (many-to-many) ``` ### ArchiveRecord (lines 53-66) Tracks processed archives to prevent re-importing. ``` Fields: id, filename, file_size, hash, imported_at, artist, tag_id ``` ### AppSettings (lines 69-78) Key-value store for application configuration. ``` Fields: id, key, value, updated_at ``` ### ImportBatch (lines 81-101) Groups import tasks for batch tracking. ``` Fields: id, source_directory, status, total_files, processed_files, imported_count, skipped_count, failed_count, started_at, completed_at ``` ### PostMetadata (lines 104-133) Stores metadata about posts from external platforms (Patreon, SubscribeStar, HentaiFoundry). ``` Fields: id, tag_id (FK to post tag), platform, post_id, artist, title, description (may contain HTML), source_url, attachment_count, published_at, created_at, updated_at Relationships: tag (one-to-one with Tag where kind='post') ``` ### SeriesPage (lines 136-168) Links images to series tags with page numbers for ordered reading. ``` Fields: id, series_tag_id (FK to tag), image_id (FK to image, unique), page_number, created_at, updated_at Constraints: unique(series_tag_id, page_number), unique(image_id) Relationships: series_tag, image ``` ### ImportTask (lines 171-222) Individual file import task tracking. ``` Fields: id, source_path, file_hash, file_size, task_type, status, celery_task_id, context (JSON), batch_id, result_image_id, error_message, retry_count, timestamps Status values: pending, queued, processing, complete, failed, skipped Task types: scan, import_image, import_archive, thumbnail, sidecar ``` --- ## Celery Tasks (`app/tasks/`) ### Scan Tasks (`app/tasks/scan.py`) | Function | Line | Purpose | |----------|------|---------| | `scan_directory()` | 29-154 | Walks `/import` directory, creates ImportTask records, queues import tasks | | `_create_task_if_new()` | 157-228 | Creates ImportTask if file not already processed | | `recover_interrupted_tasks()` | 231-302 | Re-queues stuck tasks (called periodically by Beat) | | `update_batch_stats()` | 305-349 | Updates ImportBatch statistics | ### Import Tasks (`app/tasks/import_file.py`) | Function | Line | Purpose | |----------|------|---------| | `import_media_file()` | 23-252 | Main import task - filters, deduplicates, copies, creates thumbnail | | `import_archive()` | 255-407 | Extracts archive, queues child import tasks | | `_fail_task()` | 410-425 | Marks task as failed | | `_skip_task()` | 428-443 | Marks task as skipped | | `_add_artist_tag()` | 446-459 | Adds artist:name tag to image | | `_supersede_existing()` | 472-539 | Replaces smaller image with larger version | ### Thumbnail Tasks (`app/tasks/thumbnail.py`) | Function | Line | Purpose | |----------|------|---------| | `generate_thumbnail_task()` | 18-69 | Generate thumbnail for single image/video | | `regenerate_all_thumbnails()` | 72-108 | Bulk regenerate all thumbnails | | `regenerate_missing_thumbnails()` | 111-153 | Generate only missing thumbnails | ### Sidecar Tasks (`app/tasks/sidecar.py`) | Function | Line | Purpose | |----------|------|---------| | `apply_sidecar_metadata()` | 18-97 | Apply Gallery-DL JSON metadata to image | | `scan_and_match_sidecars()` | 100-147 | Find and match sidecar files to images | | `reprocess_sidecars_for_tag()` | 150-181 | Re-run sidecar extraction for tagged images | --- ## Utility Functions (`app/utils/`) ### Image Importer (`app/utils/image_importer.py`) | Function | Line | Purpose | |----------|------|---------| | `load_import_settings()` | 50-88 | Load filter settings from DB/file/env | | `is_mostly_transparent()` | 91-146 | Check if image is mostly transparent | | `is_mostly_single_color()` | 149-215 | Check if image is solid color | | `supersede_image()` | 218-275 | Replace smaller image with larger version | | `extract_archive_resilient()` | 359-384 | Extract archive using unar/7z fallback | | `build_hashed_dest_path()` | 391-408 | Generate content-addressed file path | | `generate_thumbnail()` | 415-500 | Create thumbnail for image | | `generate_video_thumbnail_mirrored()` | 524-532 | Create thumbnail for video | | `transcode_video_to_mp4()` | 535-609 | Transcode video to H.264 MP4 | | `needs_transcode()` | 612-615 | Check if video needs transcoding | | `calculate_hash()` | 618-623 | SHA256 content hash | | `calculate_perceptual_hash()` | 626-643 | 256-bit pHash for similarity | | `find_similar_image()` | 646-678 | Find similar images by pHash | | `extract_metadata()` | 691-754 | Extract EXIF/video metadata | | `is_first_volume()` | 788-823 | Check if archive is first volume of multi-part | | `compute_next_archive_tag_name()` | 825-890 | Generate unique archive:artist/NNNN tag | ### Metadata Enrichment (`app/utils/metadata_enrichment.py`) | Function | Line | Purpose | |----------|------|---------| | `find_sidecar_json()` | 27-83 | Find Gallery-DL sidecar JSON for image | | `load_sidecar_metadata()` | 86-93 | Parse sidecar JSON file | | `extract_platform()` | 100-117 | Get platform from metadata | | `extract_artist()` | 120-136 | Get artist/creator name | | `extract_post_id()` | 139-155 | Get post ID | | `extract_post_title()` | 158-164 | Get post title | | `extract_description()` | 167-194 | Get post description/content (HTML or plain text) | | `extract_source_url()` | 197-228 | Get URL to original post (constructs for SS/HF) | | `extract_attachment_count()` | 231-248 | Get number of attachments in post | | `extract_date()` | 251-271 | Extract publication date | | `generate_source_tags()` | 338-369 | Generate source:platform and post:... tags | | `resolve_date_conflict()` | 376-405 | Keep earliest date | | `enrich_from_sidecar()` | 412-470 | Main enrichment function (returns platform, artist, title, description, source_url, etc.) | ### Version Migration (`app/utils/version_migration.py`) | Function | Line | Purpose | |----------|------|---------| | `get_setting()` / `set_setting()` | 43-74 | Read/write AppSettings | | `get_setting_int/float/bool()` | 77-104 | Typed setting getters | | `get_import_settings()` | 111-122 | Load import filter settings from DB | | `calculate_phash_256()` | 129-139 | Calculate 256-bit perceptual hash | | `recompute_all_phashes()` | 146-218 | Batch recompute all pHashes | | `check_and_run_migrations()` | 221-268 | Run data migrations on version change | --- ## HTTP Routes (`app/main.py`) ### Page Routes | Route | Line | Purpose | |-------|------|---------| | `GET /` | 18-30 | Showcase - random images masonry | | `GET /gallery` | 80-124 | Main gallery with infinite scroll | | `GET /tags` | 391-475 | Tag explorer (filterable by kind) | | `GET /artists` | 477-480 | Redirect to tag_list?kind=artist | | `GET /settings` | 488-490 | Settings page | ### Gallery API | Route | Line | Purpose | |-------|------|---------| | `GET /api/random-images` | 33-77 | Random images for showcase shuffle | | `GET /api/gallery/scroll` | 188-259 | Infinite scroll pagination | | `GET /api/gallery/timeline` | 262-316 | Year-month groups for sidebar | | `GET /api/gallery/jump` | 319-388 | Jump to specific year-month | | `GET /images/` | 127-129 | Serve image files | ### Tag API | Route | Line | Purpose | |-------|------|---------| | `GET /api/tags/search` | 497-537 | Tag autocomplete search | | `GET /image//tags` | 540-543 | List tags for image | | `POST /image//tags/add` | 546-573 | Add tag to image | | `POST /image//tags/remove` | 576-587 | Remove tag from image | | `GET /api/tag/` | 594-605 | Get tag details | | `GET /api/post-metadata/` | 608-651 | Get post metadata for a post tag | | `GET /api/post-metadata/by-tag-name/` | 654-685 | Get post metadata by tag name | | `POST /api/tag//update` | 688-764 | Update/merge tag | | `POST /api/tag//delete` | 767-776 | Delete tag | | `GET /api/tag//image-count` | 842-847 | Count images with tag | ### Bulk Operations | Route | Line | Purpose | |-------|------|---------| | `POST /api/delete-by-tag` | 703-759 | Delete all images with tag | | `POST /api/images/common-tags` | 1394-1425 | Get common tags across images | | `POST /api/images/bulk-add-tag` | 1428-1474 | Add tag to multiple images | | `POST /api/images/bulk-remove-tag` | 1477-1514 | Remove tag from multiple images | ### Series / Reader API | Route | Line | Purpose | |-------|------|---------| | `GET /read/` | 1620-1650 | Reader view for a series (vertical scroll) | | `GET /api/series//pages` | 1653-1685 | Get all pages in a series | | `POST /api/series//pages/add` | 1688-1737 | Add single image to series with page number | | `POST /api/series//pages/bulk-add` | 1740-1800 | Add multiple images with sequential page numbers | | `POST /api/series/page//update` | 1803-1828 | Update a page's page number | | `POST /api/series/page//delete` | 1831-1841 | Remove image from series | | `GET /api/image//series-info` | 1844-1858 | Check if image is in a series | ### Import Queue API | Route | Line | Purpose | |-------|------|---------| | `POST /api/import/trigger` | 961-981 | Start directory scan | | `GET /api/import/status` | 984-1042 | Queue status and recent tasks | | `GET /api/import/task/` | 1045-1082 | Task details | | `POST /api/import/retry-failed` | 1085-1122 | Retry failed tasks | | `POST /api/import/clear-completed` | 1125-1144 | Clear completed tasks | | `GET /api/import/batches` | 1222-1250 | List import batches | ### Settings API | Route | Line | Purpose | |-------|------|---------| | `GET /api/import-settings` | 774-788 | Get import filter settings | | `POST /api/import-settings` | 791-821 | Save import filter settings | | `GET /api/filter-scan` | 828-902 | Scan images against filters | | `POST /api/filter-delete` | 905-954 | Delete filtered images | ### Thumbnail API | Route | Line | Purpose | |-------|------|---------| | `POST /api/thumbnails/regenerate` | 1147-1164 | Regenerate all thumbnails | | `POST /api/thumbnails/regenerate-missing` | 1167-1183 | Generate missing thumbnails | ### Celery Status | Route | Line | Purpose | |-------|------|---------| | `GET /api/celery/status` | 1186-1219 | Worker and queue status | ### Duplicate Detection | Route | Line | Purpose | |-------|------|---------| | `POST /api/duplicates/scan` | 1257-1343 | Scan for similar images by pHash | | `POST /api/duplicates/delete` | 1346-1387 | Delete duplicate images | --- ## Frontend Templates (`app/templates/`) | Template | Purpose | |----------|---------| | `layout.html` | Base template with navbar | | `showcase.html` | Random image masonry view | | `gallery.html` | Main gallery with infinite scroll, timeline sidebar, bulk editor, series editor | | `reader.html` | Series reader with vertical scroll, page jump, thumbnail navigation | | `tags_list.html` | Tag browser with preview images | | `settings.html` | Import settings, deletion tools, duplicate detection | | `_gallery_item.html` | Single gallery item partial | | `_gallery_modal.html` | Image modal with tag editor | | `_gallery_grid.html` | Gallery grid partial | | `_pagination.html` | Pagination controls | ### JavaScript Files (`app/static/js/`) | File | Purpose | |------|---------| | `gallery-infinite.js` | Infinite scroll, timeline navigation | | `modal-pagination.js` | Modal image navigation | | `bulk-select.js` | Multi-select and bulk tag editing | | `tag-editor.js` | Tag autocomplete and editing | | `showcase.js` | Showcase shuffle functionality | --- ## Docker Services | Service | Purpose | |---------|---------| | `redis` | Message broker for Celery | | `postgres` | PostgreSQL database | | `web` | Flask application (port 5000) | | `celery-worker` | Task processor (queues: scan, import, thumbnail, sidecar, default) | | `celery-beat` | Periodic task scheduler | --- ## Key Concepts ### Content-Addressed Storage Files are stored with hash suffix: `{filename}__{hash[:10]}.{ext}` ### Perceptual Hashing 256-bit pHash for duplicate detection (threshold default: 10) ### Tag Kinds - `artist` - Artist/creator (prefix: `artist:name`) - `archive` - Archive source (prefix: `archive:artist/NNNN`) - `source` - Platform source (prefix: `source:platform`) - `post` - Post reference (prefix: `post:platform:artist:id`) - `user` - User-created tags (no prefix) - `character`, `series`, `rating` - Content tags ### Import Pipeline 1. `scan_directory` walks `/import/{artist}/` folders 2. Creates ImportTask for each file 3. `import_media_file` or `import_archive` processes file 4. Applies filters (dimensions, transparency, duplicates) 5. Copies to `/images/{artist}/` with hash suffix 6. Generates thumbnail 7. Applies sidecar metadata if found 8. Creates ImageRecord with tags ### Sidecar Metadata Gallery-DL JSON files provide: - Platform (patreon, hentaifoundry, etc.) - Artist name - Post ID and title - Publication date - Source URL --- ## Environment Variables | Variable | Default | Purpose | |----------|---------|---------| | `DB_USER` | imagerepo | PostgreSQL user | | `DB_PASS` | postgres | PostgreSQL password | | `DB_HOST` | postgres | PostgreSQL host | | `DB_NAME` | imagerepo | Database name | | `CELERY_BROKER_URL` | redis://redis:6379/0 | Redis broker | | `CELERY_WORKER_CONCURRENCY` | 2 | Worker processes | | `IMPORT_EVERY_SECONDS` | 28800 | Auto-scan interval (8h) | | `IMPORT_MIN_WIDTH` | 0 | Minimum image width | | `IMPORT_MIN_HEIGHT` | 0 | Minimum image height | | `IMPORT_SKIP_TRANSPARENT` | false | Skip transparent images | | `IMPORT_TRANSPARENCY_THRESHOLD` | 0.9 | Transparency % threshold | | `IMPORT_PHASH_THRESHOLD` | 10 | pHash similarity threshold | --- ## CLI Commands ```bash flask version-check # Run data migrations flask celery-worker # Start Celery worker flask celery-beat # Start Celery Beat scheduler ```