# app/tasks/sidecar.py """ Sidecar metadata extraction and application tasks. Handles finding and applying metadata from Gallery-DL sidecar JSON files to imported images. """ import logging from datetime import datetime from app.celery_app import celery from app import db from app.models import ImageRecord, Tag log = logging.getLogger('celery.tasks.sidecar') @celery.task(bind=True, name='app.tasks.sidecar.apply_sidecar_metadata', max_retries=2, default_retry_delay=30) def apply_sidecar_metadata(self, image_id: int, sidecar_path: str): """ Apply metadata from a sidecar JSON file to an image record. Extracts platform info, artist, post ID, dates, and generates appropriate tags (source:platform, post:platform:artist:id). Args: image_id: ID of the ImageRecord to update sidecar_path: Path to the sidecar JSON file Returns: dict with status and applied metadata info """ from app.utils.metadata_enrichment import ( enrich_from_sidecar, resolve_date_conflict, load_sidecar_metadata ) record = ImageRecord.query.get(image_id) if not record: log.warning(f"Image {image_id} not found") return {'error': 'Image not found'} try: # Load sidecar data sidecar_data = load_sidecar_metadata(sidecar_path) if not sidecar_data: return {'status': 'no_metadata', 'reason': 'Failed to load sidecar'} # Get enriched metadata enriched = enrich_from_sidecar(sidecar_path, {'taken_at': record.taken_at}) if not enriched.get('raw_metadata'): return {'status': 'no_metadata', 'reason': 'No metadata extracted'} tags_added = 0 # Update date if earlier new_date = resolve_date_conflict(record.taken_at, enriched.get('taken_at')) if new_date != record.taken_at: record.taken_at = new_date log.debug(f"Updated date for {image_id}: {new_date}") # Apply tags from metadata for tag_info in enriched.get('tags', []): tag_name = tag_info.get('name') tag_kind = tag_info.get('kind') if not tag_name: continue tag = Tag.query.filter_by(name=tag_name).first() if not tag: tag = Tag(name=tag_name, kind=tag_kind) db.session.add(tag) db.session.flush() log.debug(f"Created tag: {tag_name} ({tag_kind})") if tag not in record.tags: record.tags.append(tag) tags_added += 1 db.session.commit() log.info(f"Applied sidecar metadata to {image_id}: " f"platform={enriched.get('platform')}, tags_added={tags_added}") return { 'status': 'applied', 'platform': enriched.get('platform'), 'artist': enriched.get('artist'), 'post_id': enriched.get('post_id'), 'tags_added': tags_added } except Exception as e: db.session.rollback() log.error(f"Sidecar application failed for {image_id}: {e}", exc_info=True) raise self.retry(exc=e) @celery.task(name='app.tasks.sidecar.scan_and_match_sidecars') def scan_and_match_sidecars(batch_id: int = None, limit: int = 1000): """ Scan for sidecar files and match them to imported images. Looks at recently imported images and tries to find corresponding sidecar JSON files for metadata enrichment. Args: batch_id: Optional batch ID to limit scope limit: Maximum number of images to process Returns: dict with count of matched sidecars """ from app.utils.metadata_enrichment import find_sidecar_json log.info(f"Scanning for unmatched sidecars (batch={batch_id}, limit={limit})") # Build query for images that might need sidecar matching query = ImageRecord.query.filter( ImageRecord.filepath.isnot(None) ).order_by(ImageRecord.imported_at.desc()) if limit: query = query.limit(limit) matched = 0 already_has_source = 0 for record in query.all(): # Check if already has source tag (indicates sidecar was processed) has_source_tag = any( tag.kind == 'source' or tag.kind == 'post' for tag in record.tags ) if has_source_tag: already_has_source += 1 continue # Try to find sidecar sidecar_path = find_sidecar_json(record.filepath) if sidecar_path: apply_sidecar_metadata.delay(record.id, sidecar_path) matched += 1 log.info(f"Sidecar scan complete: matched={matched}, already_tagged={already_has_source}") return {'matched': matched, 'already_tagged': already_has_source} @celery.task(name='app.tasks.sidecar.reprocess_sidecars_for_tag') def reprocess_sidecars_for_tag(tag_name: str): """ Reprocess sidecar metadata for all images with a specific tag. Useful for re-running sidecar extraction after updates to the metadata extraction logic. Args: tag_name: Name of the tag to filter by Returns: dict with count of queued tasks """ from app.utils.metadata_enrichment import find_sidecar_json tag = Tag.query.filter_by(name=tag_name).first() if not tag: return {'error': f'Tag not found: {tag_name}'} queued = 0 for record in tag.images: if not record.filepath: continue sidecar_path = find_sidecar_json(record.filepath) if sidecar_path: apply_sidecar_metadata.delay(record.id, sidecar_path) queued += 1 log.info(f"Queued {queued} sidecar reprocessing tasks for tag: {tag_name}") return {'queued': queued, 'tag': tag_name}