"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var MediaRepository_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MediaRepository = void 0;
const common_1 = require("@nestjs/common");
const exiftool_vendored_1 = require("exiftool-vendored");
const fluent_ffmpeg_1 = __importDefault(require("fluent-ffmpeg"));
const luxon_1 = require("luxon");
const promises_1 = __importDefault(require("node:fs/promises"));
const node_stream_1 = require("node:stream");
const sharp_1 = __importDefault(require("sharp"));
const constants_1 = require("../constants");
const enum_1 = require("../enum");
const logging_repository_1 = require("./logging.repository");
const misc_1 = require("../utils/misc");
const transform_1 = require("../utils/transform");
const probe = (input, options) => new Promise((resolve, reject) => fluent_ffmpeg_1.default.ffprobe(input, options, (error, data) => (error ? reject(error) : resolve(data))));
sharp_1.default.concurrency(0);
sharp_1.default.cache({ files: 0 });
let MediaRepository = MediaRepository_1 = class MediaRepository {
    logger;
    constructor(logger) {
        this.logger = logger;
        this.logger.setContext(MediaRepository_1.name);
    }
    async extract(input) {
        try {
            const buffer = await exiftool_vendored_1.exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input);
            return { buffer, format: enum_1.RawExtractedFormat.Jpeg };
        }
        catch (error) {
            this.logger.debug(`Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next: ${error}`);
        }
        try {
            const buffer = await exiftool_vendored_1.exiftool.extractBinaryTagToBuffer('JpgFromRaw', input);
            return { buffer, format: enum_1.RawExtractedFormat.Jpeg };
        }
        catch (error) {
            this.logger.debug(`Could not extract JPEG buffer from image, trying PreviewJXL next: ${error}`);
        }
        try {
            const buffer = await exiftool_vendored_1.exiftool.extractBinaryTagToBuffer('PreviewJXL', input);
            return { buffer, format: enum_1.RawExtractedFormat.Jxl };
        }
        catch (error) {
            this.logger.debug(`Could not extract PreviewJXL buffer from image, trying PreviewImage next: ${error}`);
        }
        try {
            const buffer = await exiftool_vendored_1.exiftool.extractBinaryTagToBuffer('PreviewImage', input);
            return { buffer, format: enum_1.RawExtractedFormat.Jpeg };
        }
        catch (error) {
            this.logger.debug(`Could not extract preview buffer from image: ${error}`);
            return null;
        }
    }
    async writeExif(tags, output) {
        try {
            const tagsToWrite = {
                ExifImageWidth: tags.exifImageWidth,
                ExifImageHeight: tags.exifImageHeight,
                DateTimeOriginal: tags.dateTimeOriginal && exiftool_vendored_1.ExifDateTime.fromMillis(tags.dateTimeOriginal.getTime()),
                ModifyDate: tags.modifyDate && exiftool_vendored_1.ExifDateTime.fromMillis(tags.modifyDate.getTime()),
                TimeZone: tags.timeZone,
                GPSLatitude: tags.latitude,
                GPSLongitude: tags.longitude,
                ProjectionType: tags.projectionType,
                City: tags.city,
                Country: tags.country,
                Make: tags.make,
                Model: tags.model,
                LensModel: tags.lensModel,
                Fnumber: tags.fNumber?.toFixed(1),
                FocalLength: tags.focalLength?.toFixed(1),
                ISO: tags.iso,
                ExposureTime: tags.exposureTime,
                ProfileDescription: tags.profileDescription,
                ColorSpace: tags.colorspace,
                Rating: tags.rating === null ? 0 : tags.rating,
                'Orientation#': tags.orientation ? Number(tags.orientation) : undefined,
            };
            await exiftool_vendored_1.exiftool.write(output, tagsToWrite, {
                ignoreMinorErrors: true,
                writeArgs: ['-overwrite_original'],
            });
            return true;
        }
        catch (error) {
            this.logger.warn(`Could not write exif data to image: ${error.message}`);
            return false;
        }
    }
    async copyTagGroup(tagGroup, source, target) {
        try {
            await exiftool_vendored_1.exiftool.write(target, {}, {
                ignoreMinorErrors: true,
                writeArgs: ['-TagsFromFile', source, `-${tagGroup}:all>${tagGroup}:all`, '-overwrite_original'],
            });
            return true;
        }
        catch (error) {
            this.logger.warn(`Could not copy tag data to image: ${error.message}`);
            return false;
        }
    }
    async decodeImage(input, options) {
        const pipeline = await this.getImageDecodingPipeline(input, options);
        return pipeline.raw().toBuffer({ resolveWithObject: true });
    }
    async applyEdits(pipeline, edits) {
        const affineEditOperations = edits.filter((edit) => edit.action !== 'crop');
        const matrix = (0, transform_1.createAffineMatrix)(affineEditOperations);
        const crop = edits.find((edit) => edit.action === 'crop');
        const dimensions = await pipeline.metadata();
        if (crop) {
            pipeline = pipeline.extract({
                left: crop ? Math.round(crop.parameters.x) : 0,
                top: crop ? Math.round(crop.parameters.y) : 0,
                width: crop ? Math.round(crop.parameters.width) : dimensions.width || 0,
                height: crop ? Math.round(crop.parameters.height) : dimensions.height || 0,
            });
        }
        const { a, b, c, d } = matrix;
        pipeline = pipeline.affine([
            [a, b],
            [c, d],
        ]);
        return pipeline;
    }
    async generateThumbnail(input, options, output) {
        const pipeline = await this.getImageDecodingPipeline(input, options);
        const decoded = pipeline.toFormat(options.format, {
            quality: options.quality,
            chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0',
            progressive: options.progressive,
        });
        await decoded.toFile(output);
    }
    async getImageDecodingPipeline(input, options) {
        let pipeline = (0, sharp_1.default)(input, {
            failOn: options.processInvalidImages ? 'none' : 'error',
            limitInputPixels: false,
            raw: options.raw,
            unlimited: true,
        })
            .pipelineColorspace(options.colorspace === enum_1.Colorspace.Srgb ? 'srgb' : 'rgb16')
            .withIccProfile(options.colorspace);
        if (!options.raw) {
            const { angle, flip, flop } = options.orientation ? constants_1.ORIENTATION_TO_SHARP_ROTATION[options.orientation] : {};
            pipeline = pipeline.rotate(angle);
            if (flip) {
                pipeline = pipeline.flip();
            }
            if (flop) {
                pipeline = pipeline.flop();
            }
        }
        if (options.edits && options.edits.length > 0) {
            pipeline = await this.applyEdits(pipeline, options.edits);
        }
        if (options.size !== undefined) {
            pipeline = pipeline.resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true });
        }
        return pipeline;
    }
    async generateThumbhash(input, options) {
        const [{ rgbaToThumbHash }, decodingPipeline] = await Promise.all([
            import('thumbhash'),
            this.getImageDecodingPipeline(input, {
                colorspace: options.colorspace,
                processInvalidImages: options.processInvalidImages,
                raw: options.raw,
                edits: options.edits,
            }),
        ]);
        const pipeline = decodingPipeline.resize(100, 100, { fit: 'inside', withoutEnlargement: true }).raw().ensureAlpha();
        const { data, info } = await pipeline.toBuffer({ resolveWithObject: true });
        return Buffer.from(rgbaToThumbHash(info.width, info.height, data));
    }
    async probe(input, options) {
        const results = await probe(input, options?.countFrames ? ['-count_packets'] : []);
        return {
            format: {
                formatName: results.format.format_name,
                formatLongName: results.format.format_long_name,
                duration: this.parseFloat(results.format.duration),
                bitrate: this.parseInt(results.format.bit_rate),
            },
            videoStreams: results.streams
                .filter((stream) => stream.codec_type === 'video' && !stream.disposition?.attached_pic)
                .map((stream) => {
                const height = this.parseInt(stream.height);
                const dar = this.getDar(stream.display_aspect_ratio);
                return {
                    index: stream.index,
                    height,
                    width: dar ? Math.round(height * dar) : this.parseInt(stream.width),
                    codecName: stream.codec_name === 'h265' ? 'hevc' : stream.codec_name,
                    codecType: stream.codec_type,
                    frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames),
                    rotation: this.parseInt(stream.rotation),
                    isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67',
                    bitrate: this.parseInt(stream.bit_rate),
                    pixelFormat: stream.pix_fmt || 'yuv420p',
                    colorPrimaries: stream.color_primaries,
                    colorSpace: stream.color_space,
                    colorTransfer: stream.color_transfer,
                };
            }),
            audioStreams: results.streams
                .filter((stream) => stream.codec_type === 'audio')
                .map((stream) => ({
                index: stream.index,
                codecType: stream.codec_type,
                codecName: stream.codec_name,
                bitrate: this.parseInt(stream.bit_rate),
            })),
        };
    }
    transcode(input, output, options) {
        if (!options.twoPass) {
            return new Promise((resolve, reject) => {
                this.configureFfmpegCall(input, output, options)
                    .on('error', reject)
                    .on('end', () => resolve())
                    .run();
            });
        }
        if (typeof output !== 'string') {
            throw new TypeError('Two-pass transcoding does not support writing to a stream');
        }
        return new Promise((resolve, reject) => {
            this.configureFfmpegCall(input, '/dev/null', options)
                .addOptions('-pass', '1')
                .addOptions('-passlogfile', output)
                .addOptions('-f null')
                .on('error', reject)
                .on('end', () => {
                this.configureFfmpegCall(input, output, options)
                    .addOptions('-pass', '2')
                    .addOptions('-passlogfile', output)
                    .on('error', reject)
                    .on('end', () => (0, misc_1.handlePromiseError)(promises_1.default.unlink(`${output}-0.log`), this.logger))
                    .on('end', () => (0, misc_1.handlePromiseError)(promises_1.default.rm(`${output}-0.log.mbtree`, { force: true }), this.logger))
                    .on('end', () => resolve())
                    .run();
            })
                .run();
        });
    }
    async getImageMetadata(input) {
        const { width = 0, height = 0, hasAlpha = false } = await (0, sharp_1.default)(input).metadata();
        return { width, height, isTransparent: hasAlpha };
    }
    configureFfmpegCall(input, output, options) {
        const ffmpegCall = (0, fluent_ffmpeg_1.default)(input, { niceness: 10 })
            .inputOptions(options.inputOptions)
            .outputOptions(options.outputOptions)
            .output(output)
            .on('start', (command) => this.logger.debug(command))
            .on('error', (error, _, stderr) => this.logger.error(stderr || error));
        const { frameCount, percentInterval } = options.progress;
        const frameInterval = Math.ceil(frameCount / (100 / percentInterval));
        if (this.logger.isLevelEnabled(enum_1.LogLevel.Debug) && frameCount && frameInterval) {
            let lastProgressFrame = 0;
            ffmpegCall.on('progress', (progress) => {
                if (progress.frames - lastProgressFrame < frameInterval) {
                    return;
                }
                lastProgressFrame = progress.frames;
                const percent = ((progress.frames / frameCount) * 100).toFixed(2);
                const ms = progress.currentFps ? Math.floor((frameCount - progress.frames) / progress.currentFps) * 1000 : 0;
                const duration = ms ? luxon_1.Duration.fromMillis(ms).rescale().toHuman({ unitDisplay: 'narrow' }) : '';
                const outputText = output instanceof node_stream_1.Writable ? 'stream' : output.split('/').pop();
                this.logger.debug(`Transcoding ${percent}% done${duration ? `, estimated ${duration} remaining` : ''} for output ${outputText}`);
            });
        }
        return ffmpegCall;
    }
    parseInt(value) {
        return Number.parseInt(value) || 0;
    }
    parseFloat(value) {
        return Number.parseFloat(value) || 0;
    }
    getDar(dar) {
        if (dar) {
            const [darW, darH] = dar.split(':').map(Number);
            if (darW && darH) {
                return darW / darH;
            }
        }
        return 0;
    }
};
exports.MediaRepository = MediaRepository;
exports.MediaRepository = MediaRepository = MediaRepository_1 = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [logging_repository_1.LoggingRepository])
], MediaRepository);
//# sourceMappingURL=media.repository.js.map