mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
Feature - Add upload functionality on Web (#231)
* Added file selector * Extract metadata to upload files to the web * Added request for uploading * Generate jpeg/Webp thumbnail for asset uploaded without thumbnail data * Added generating thumbnail for video and WebSocket broadcast after thumbnail is generated * Added video length extraction * Added Uploading Panel * Added upload progress store and styling the uploaded asset * Added condition to only show upload panel when there is upload in progress * Remove asset from the upload list after successfully uploading * Added WebSocket to listen to upload event on the web * Added mechanism to check for existing assets before uploading on the web * Added test workflow * Update readme
This commit is contained in:
parent
b7603fd150
commit
1e3464fe47
33 changed files with 859 additions and 220 deletions
|
|
@ -46,6 +46,7 @@ export class AssetUploadedProcessor {
|
|||
await this.metadataExtractionQueue.add('detect-object', { asset }, { jobId: randomUUID() });
|
||||
} else {
|
||||
// Generate Thumbnail -> Then generate webp, tag image and detect object
|
||||
await this.thumbnailGeneratorQueue.add('generate-jpeg-thumbnail', { asset }, { jobId: randomUUID() });
|
||||
}
|
||||
|
||||
// Video Conversion
|
||||
|
|
@ -63,5 +64,10 @@ export class AssetUploadedProcessor {
|
|||
{ jobId: randomUUID() },
|
||||
);
|
||||
}
|
||||
|
||||
// Extract video duration if uploaded from the web
|
||||
if (asset.type == AssetType.VIDEO && asset.duration == '0:00:00.000000') {
|
||||
await this.metadataExtractionQueue.add('extract-video-length', { asset }, { jobId: randomUUID() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import { Logger } from '@nestjs/common';
|
|||
import axios from 'axios';
|
||||
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
// import moment from 'moment';
|
||||
|
||||
@Processor('metadata-extraction-queue')
|
||||
export class MetadataExtractionProcessor {
|
||||
|
|
@ -129,4 +131,27 @@ export class MetadataExtractionProcessor {
|
|||
Logger.error(`Failed to trigger object detection pipe line ${error.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@Process({ name: 'extract-video-length', concurrency: 2 })
|
||||
async extractVideoLength(job: Job) {
|
||||
const { asset }: { asset: AssetEntity } = job.data;
|
||||
|
||||
ffmpeg.ffprobe(asset.originalPath, async (err, data) => {
|
||||
if (!err) {
|
||||
if (data.format.duration) {
|
||||
const videoDurationInSecond = parseInt(data.format.duration.toString(), 0);
|
||||
|
||||
const hours = Math.floor(videoDurationInSecond / 3600);
|
||||
const minutes = Math.floor((videoDurationInSecond - hours * 3600) / 60);
|
||||
const seconds = videoDurationInSecond - hours * 3600 - minutes * 60;
|
||||
|
||||
const durationString = `${hours}:${minutes < 10 ? '0' + minutes.toString() : minutes}:${
|
||||
seconds < 10 ? '0' + seconds.toString() : seconds
|
||||
}.000000`;
|
||||
|
||||
await this.assetRepository.update({ id: asset.id }, { duration: durationString });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,82 @@
|
|||
import { Process, Processor } from '@nestjs/bull';
|
||||
import { Job } from 'bull';
|
||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
||||
import { Job, Queue } from 'bull';
|
||||
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
||||
import { Repository } from 'typeorm/repository/Repository';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import sharp from 'sharp';
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { CommunicationGateway } from '../../../immich/src/api-v1/communication/communication.gateway';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
@Processor('thumbnail-generator-queue')
|
||||
export class ThumbnailGeneratorProcessor {
|
||||
constructor(
|
||||
@InjectRepository(AssetEntity)
|
||||
private assetRepository: Repository<AssetEntity>,
|
||||
|
||||
@InjectQueue('thumbnail-generator-queue')
|
||||
private thumbnailGeneratorQueue: Queue,
|
||||
|
||||
private wsCommunicateionGateway: CommunicationGateway,
|
||||
) {}
|
||||
|
||||
@Process('generate-jpeg-thumbnail')
|
||||
async generateJPEGThumbnail(job: Job) {
|
||||
const { asset }: { asset: AssetEntity } = job.data;
|
||||
|
||||
console.log(asset);
|
||||
const resizePath = `upload/${asset.userId}/thumb/${asset.deviceId}/`;
|
||||
|
||||
if (!existsSync(resizePath)) {
|
||||
mkdirSync(resizePath, { recursive: true });
|
||||
}
|
||||
|
||||
const temp = asset.originalPath.split('/');
|
||||
const originalFilename = temp[temp.length - 1].split('.')[0];
|
||||
const jpegThumbnailPath = resizePath + originalFilename + '.jpeg';
|
||||
|
||||
if (asset.type == AssetType.IMAGE) {
|
||||
sharp(asset.originalPath)
|
||||
.resize(1440, 2560, { fit: 'inside' })
|
||||
.jpeg()
|
||||
.toFile(jpegThumbnailPath, async (err, info) => {
|
||||
if (!err) {
|
||||
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
||||
|
||||
// Update resize path to send to generate webp queue
|
||||
asset.resizePath = jpegThumbnailPath;
|
||||
|
||||
await this.thumbnailGeneratorQueue.add('generate-webp-thumbnail', { asset }, { jobId: randomUUID() });
|
||||
this.wsCommunicateionGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(asset));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (asset.type == AssetType.VIDEO) {
|
||||
ffmpeg(asset.originalPath)
|
||||
.outputOptions(['-ss 00:00:01.000', '-frames:v 1'])
|
||||
.output(jpegThumbnailPath)
|
||||
.on('start', () => {
|
||||
Logger.log('Start Generating Video Thumbnail', 'generateJPEGThumbnail');
|
||||
})
|
||||
.on('error', (error, b, c) => {
|
||||
Logger.error(`Cannot Generate Video Thumbnail ${error}`, 'generateJPEGThumbnail');
|
||||
// reject();
|
||||
})
|
||||
.on('end', async () => {
|
||||
Logger.log(`Generating Video Thumbnail Success ${asset.id}`, 'generateJPEGThumbnail');
|
||||
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
||||
|
||||
// Update resize path to send to generate webp queue
|
||||
asset.resizePath = jpegThumbnailPath;
|
||||
|
||||
await this.thumbnailGeneratorQueue.add('generate-webp-thumbnail', { asset }, { jobId: randomUUID() });
|
||||
|
||||
this.wsCommunicateionGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(asset));
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Process({ name: 'generate-webp-thumbnail', concurrency: 2 })
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export class VideoTranscodeProcessor {
|
|||
.outputOptions(['-crf 23', '-preset ultrafast', '-vcodec libx264', '-acodec mp3', '-vf scale=1280:-2'])
|
||||
.output(savedEncodedPath)
|
||||
.on('start', () => {
|
||||
Logger.log('Start Converting', 'mp4Conversion');
|
||||
Logger.log('Start Converting Video', 'mp4Conversion');
|
||||
})
|
||||
.on('error', (error, b, c) => {
|
||||
Logger.error(`Cannot Convert Video ${error}`, 'mp4Conversion');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue