mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
Get thumbnail from app (#68)
* Renamed multipart filed name 'files' to 'assetData'. * Added an additional field name of 'thumbnailData' to multipart form. * Implemented upload mechanism for thumbnail directly from the mobile client. * Removed dead code * Implemented a version checking mechanism.
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||
import { AssetService } from './asset.service';
|
||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { FileFieldsInterceptor, FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { multerOption } from '../../config/multer-option.config';
|
||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||
@@ -29,34 +29,43 @@ import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
|
||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||
import { SearchAssetDto } from './dto/search-asset.dto';
|
||||
import { CommunicationGateway } from '../communication/communication.gateway';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('asset')
|
||||
export class AssetController {
|
||||
constructor(
|
||||
private wsCommunicateionGateway: CommunicationGateway,
|
||||
private assetService: AssetService,
|
||||
private assetOptimizeService: AssetOptimizeService,
|
||||
private backgroundTaskService: BackgroundTaskService,
|
||||
) {}
|
||||
|
||||
@Post('upload')
|
||||
@UseInterceptors(FilesInterceptor('files', 30, multerOption))
|
||||
@UseInterceptors(
|
||||
FileFieldsInterceptor(
|
||||
[
|
||||
{ name: 'assetData', maxCount: 1 },
|
||||
{ name: 'thumbnailData', maxCount: 1 },
|
||||
],
|
||||
multerOption,
|
||||
),
|
||||
)
|
||||
async uploadFile(
|
||||
@GetAuthUser() authUser,
|
||||
@UploadedFiles() files: Express.Multer.File[],
|
||||
@UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] },
|
||||
@Body(ValidationPipe) assetInfo: CreateAssetDto,
|
||||
) {
|
||||
files.forEach(async (file) => {
|
||||
uploadFiles.assetData.forEach(async (file) => {
|
||||
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
|
||||
|
||||
if (savedAsset && savedAsset.type == AssetType.IMAGE) {
|
||||
await this.assetOptimizeService.resizeImage(savedAsset);
|
||||
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
|
||||
if (uploadFiles.thumbnailData != null) {
|
||||
await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path);
|
||||
await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset);
|
||||
}
|
||||
|
||||
if (savedAsset && savedAsset.type == AssetType.VIDEO) {
|
||||
await this.assetOptimizeService.getVideoThumbnail(savedAsset, file.originalname);
|
||||
}
|
||||
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
|
||||
|
||||
this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset));
|
||||
});
|
||||
|
||||
return 'ok';
|
||||
|
||||
@@ -8,9 +8,12 @@ import { AssetOptimizeService } from '../../modules/image-optimize/image-optimiz
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
|
||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||
import { CommunicationModule } from '../communication/communication.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CommunicationModule,
|
||||
|
||||
BullModule.registerQueue({
|
||||
name: 'optimize',
|
||||
defaultJobOptions: {
|
||||
|
||||
@@ -24,6 +24,12 @@ export class AssetService {
|
||||
private assetRepository: Repository<AssetEntity>,
|
||||
) {}
|
||||
|
||||
public async updateThumbnailInfo(assetId: string, path: string) {
|
||||
return await this.assetRepository.update(assetId, {
|
||||
resizePath: path,
|
||||
});
|
||||
}
|
||||
|
||||
public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
|
||||
const asset = new AssetEntity();
|
||||
asset.deviceAssetId = assetInfo.deviceAssetId;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||
import { ServerInfoService } from './server-info.service';
|
||||
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
||||
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
||||
import { serverVersion } from '../../constants/server_version.constant';
|
||||
|
||||
@Controller('server-info')
|
||||
export class ServerInfoController {
|
||||
@@ -30,4 +31,9 @@ export class ServerInfoController {
|
||||
mapboxSecret: this.configService.get('MAPBOX_KEY'),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/version')
|
||||
async getServerVersion() {
|
||||
return serverVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,33 @@ export const multerOption: MulterOptions = {
|
||||
destination: (req: Request, file: Express.Multer.File, cb: any) => {
|
||||
const uploadPath = multerConfig.dest;
|
||||
|
||||
const userPath = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
|
||||
if (file.fieldname == 'assetData') {
|
||||
const originalUploadFolder = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
|
||||
|
||||
if (!existsSync(userPath)) {
|
||||
mkdirSync(userPath, { recursive: true });
|
||||
if (!existsSync(originalUploadFolder)) {
|
||||
mkdirSync(originalUploadFolder, { recursive: true });
|
||||
}
|
||||
|
||||
cb(null, originalUploadFolder);
|
||||
} else if (file.fieldname == 'thumbnailData') {
|
||||
const thumbnailUploadFolder = `${uploadPath}/${req.user['id']}/thumb/${req.body['deviceId']}`;
|
||||
|
||||
if (!existsSync(thumbnailUploadFolder)) {
|
||||
mkdirSync(thumbnailUploadFolder, { recursive: true });
|
||||
}
|
||||
|
||||
cb(null, thumbnailUploadFolder);
|
||||
}
|
||||
|
||||
cb(null, userPath);
|
||||
},
|
||||
|
||||
filename: (req: Request, file: Express.Multer.File, cb: any) => {
|
||||
cb(null, `${file.originalname.split('.')[0]}${req.body['fileExtension']}`);
|
||||
// console.log(req, file);
|
||||
|
||||
if (file.fieldname == 'assetData') {
|
||||
cb(null, `${file.originalname.split('.')[0]}${req.body['fileExtension']}`);
|
||||
} else if (file.fieldname == 'thumbnailData') {
|
||||
cb(null, `${file.originalname.split('.')[0]}.jpeg`);
|
||||
}
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
9
server/src/constants/server_version.constant.ts
Normal file
9
server/src/constants/server_version.constant.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// major.minor.patch+build
|
||||
// check mobile/pubspec.yml for current release version
|
||||
|
||||
export const serverVersion = {
|
||||
major: 1,
|
||||
minor: 3,
|
||||
patch: 0,
|
||||
build: 0,
|
||||
};
|
||||
@@ -41,7 +41,6 @@ export class BackgroundTaskProcessor {
|
||||
async extractExif(job: Job) {
|
||||
const { savedAsset, fileName, fileSize }: { savedAsset: AssetEntity; fileName: string; fileSize: number } =
|
||||
job.data;
|
||||
|
||||
const fileBuffer = await readFile(savedAsset.originalPath);
|
||||
|
||||
const exifData = await exifr.parse(fileBuffer);
|
||||
|
||||
@@ -22,122 +22,4 @@ export class ImageOptimizeProcessor {
|
||||
|
||||
private backgroundTaskService: BackgroundTaskService,
|
||||
) {}
|
||||
|
||||
@Process('resize-image')
|
||||
async resizeUploadedImage(job: Job) {
|
||||
const { savedAsset }: { savedAsset: AssetEntity } = job.data;
|
||||
|
||||
const basePath = APP_UPLOAD_LOCATION;
|
||||
const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
|
||||
|
||||
// Create folder for thumb image if not exist
|
||||
|
||||
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
|
||||
|
||||
if (!existsSync(resizeDir)) {
|
||||
mkdirSync(resizeDir, { recursive: true });
|
||||
}
|
||||
|
||||
readFile(savedAsset.originalPath, async (err, data) => {
|
||||
if (err) {
|
||||
console.error('Error Reading File');
|
||||
}
|
||||
|
||||
// Special Assets Type - ios
|
||||
if (
|
||||
savedAsset.mimeType == 'image/heic' ||
|
||||
savedAsset.mimeType == 'image/heif' ||
|
||||
savedAsset.mimeType == 'image/dng'
|
||||
) {
|
||||
let desitnation = '';
|
||||
if (savedAsset.mimeType == 'image/heic') {
|
||||
desitnation = resizePath.replace('.HEIC', '.jpeg');
|
||||
} else if (savedAsset.mimeType == 'image/heif') {
|
||||
desitnation = resizePath.replace('.HEIF', '.jpeg');
|
||||
} else if (savedAsset.mimeType == 'image/dng') {
|
||||
desitnation = resizePath.replace('.DNG', '.jpeg');
|
||||
}
|
||||
|
||||
sharp(data)
|
||||
.toFormat('jpeg')
|
||||
.resize(512, 512, { fit: 'outside' })
|
||||
.toFile(desitnation, async (err, info) => {
|
||||
if (err) {
|
||||
console.error('Error resizing file ', err);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await this.assetRepository.update(savedAsset, { resizePath: desitnation });
|
||||
|
||||
if (res.affected) {
|
||||
this.wsCommunicateionGateway.server
|
||||
.to(savedAsset.userId)
|
||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
||||
}
|
||||
|
||||
// Tag Image
|
||||
this.backgroundTaskService.tagImage(desitnation, savedAsset);
|
||||
});
|
||||
} else {
|
||||
sharp(data)
|
||||
.resize(512, 512, { fit: 'outside' })
|
||||
.toFile(resizePath, async (err, info) => {
|
||||
if (err) {
|
||||
console.error('Error resizing file ', err);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await this.assetRepository.update(savedAsset, { resizePath: resizePath });
|
||||
if (res.affected) {
|
||||
this.wsCommunicateionGateway.server
|
||||
.to(savedAsset.userId)
|
||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
||||
}
|
||||
|
||||
// Tag Image
|
||||
this.backgroundTaskService.tagImage(resizePath, savedAsset);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
@Process('get-video-thumbnail')
|
||||
async resizeUploadedVideo(job: Job) {
|
||||
const { savedAsset, filename }: { savedAsset: AssetEntity; filename: String } = job.data;
|
||||
|
||||
const basePath = APP_UPLOAD_LOCATION;
|
||||
// const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
|
||||
// Create folder for thumb image if not exist
|
||||
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
|
||||
|
||||
if (!existsSync(resizeDir)) {
|
||||
mkdirSync(resizeDir, { recursive: true });
|
||||
}
|
||||
|
||||
ffmpeg(savedAsset.originalPath)
|
||||
.thumbnail({
|
||||
count: 1,
|
||||
timestamps: [1],
|
||||
folder: resizeDir,
|
||||
filename: `${filename}.png`,
|
||||
})
|
||||
.on('end', async (a) => {
|
||||
const thumbnailPath = `${resizeDir}/${filename}.png`;
|
||||
|
||||
const res = await this.assetRepository.update(savedAsset, { resizePath: `${resizeDir}/${filename}.png` });
|
||||
|
||||
if (res.affected) {
|
||||
this.wsCommunicateionGateway.server
|
||||
.to(savedAsset.userId)
|
||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
||||
}
|
||||
|
||||
// Tag Image
|
||||
this.backgroundTaskService.tagImage(thumbnailPath, savedAsset);
|
||||
});
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,33 +7,4 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
|
||||
@Injectable()
|
||||
export class AssetOptimizeService {
|
||||
constructor(@InjectQueue('optimize') private optimizeQueue: Queue) {}
|
||||
|
||||
public async resizeImage(savedAsset: AssetEntity) {
|
||||
const job = await this.optimizeQueue.add(
|
||||
'resize-image',
|
||||
{
|
||||
savedAsset,
|
||||
},
|
||||
{ jobId: randomUUID() },
|
||||
);
|
||||
|
||||
return {
|
||||
jobId: job.id,
|
||||
};
|
||||
}
|
||||
|
||||
public async getVideoThumbnail(savedAsset: AssetEntity, filename: string) {
|
||||
const job = await this.optimizeQueue.add(
|
||||
'get-video-thumbnail',
|
||||
{
|
||||
savedAsset,
|
||||
filename,
|
||||
},
|
||||
{ jobId: randomUUID() },
|
||||
);
|
||||
|
||||
return {
|
||||
jobId: job.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user