managarten/services/mana-media/apps/api/src/modules/process/process.worker.ts
Till-JS 90c2f8573e feat(photos): add Photos app with mana-media EXIF integration
- Add Photos NestJS backend (port 3019) with albums, favorites, tags
- Add Photos SvelteKit web app (port 5189) with gallery, upload, filters
- Extend mana-media with EXIF extraction service using exifr
- Add cross-app photo listing endpoint to mana-media
- Add photo stats endpoint to mana-media
- Add photos to setup-databases.sh

Backend features:
- Albums CRUD with cover image and items management
- Favorites toggle with status check
- Tags CRUD with photo-tag associations
- Photo proxy to mana-media with local data enrichment

Web features:
- Photo grid with infinite scroll
- Photo detail modal with EXIF display
- Album grid and detail views
- Upload dropzone with progress tracking
- Filter bar (app, date range, location, sort)
- i18n support (de/en)
- Svelte 5 runes mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 17:58:44 +01:00

77 lines
2.3 KiB
TypeScript

import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Logger } from '@nestjs/common';
import { Job } from 'bullmq';
import { ProcessService } from './process.service';
import { UploadService } from '../upload/upload.service';
import { PROCESS_QUEUE, SUPPORTED_IMAGE_TYPES } from './process.constants';
interface ProcessJobData {
mediaId: string;
mimeType: string;
originalKey: string;
}
@Processor(PROCESS_QUEUE)
export class ProcessWorker extends WorkerHost {
private readonly logger = new Logger(ProcessWorker.name);
constructor(
private processService: ProcessService,
private uploadService: UploadService
) {
super();
}
async process(job: Job<ProcessJobData>): Promise<void> {
const { mediaId, mimeType, originalKey } = job.data;
this.logger.log(`Processing media ${mediaId} (${mimeType})`);
try {
if (SUPPORTED_IMAGE_TYPES.includes(mimeType)) {
await this.processImage(mediaId, originalKey, mimeType);
} else {
// For unsupported types, just mark as ready
await this.uploadService.update(mediaId, { status: 'ready' });
}
} catch (error) {
this.logger.error(`Failed to process media ${mediaId}:`, error);
await this.uploadService.update(mediaId, { status: 'failed' });
throw error;
}
}
private async processImage(
mediaId: string,
originalKey: string,
mimeType: string
): Promise<void> {
const result = await this.processService.processImage(mediaId, originalKey, mimeType);
await this.uploadService.update(mediaId, {
status: 'ready',
thumbnailKey: result.thumbnail,
mediumKey: result.medium,
largeKey: result.large,
width: result.metadata?.width,
height: result.metadata?.height,
format: result.metadata?.format,
hasAlpha: result.metadata?.hasAlpha,
// EXIF data
exifData: result.exif?.raw,
dateTaken: result.exif?.dateTaken,
cameraMake: result.exif?.cameraMake,
cameraModel: result.exif?.cameraModel,
focalLength: result.exif?.focalLength,
aperture: result.exif?.aperture,
iso: result.exif?.iso,
exposureTime: result.exif?.exposureTime,
gpsLatitude: result.exif?.gpsLatitude,
gpsLongitude: result.exif?.gpsLongitude,
});
this.logger.log(
`Processed image ${mediaId}: thumbnail=${!!result.thumbnail}, medium=${!!result.medium}, large=${!!result.large}, exif=${!!result.exif}`
);
}
}