소스 검색

Add webp thumbnail conversion task to optimize performance of fast scrolling (#172)

* Update readme

* Added webp to table and entity

* Added cronjob and sharp dependencies

* Added conversion of webp every 5 minutes and endpoint will now server webp image if exist
Alex 3 년 전
부모
커밋
55c5027539

+ 12 - 1
README.md

@@ -86,7 +86,7 @@ I haven't tested with `Docker for Windows` as well as `WSL` on Windows
 
 **Core**: At least 2 cores, preffered 4 cores.
 
-# Development and Testing out the application
+# Getting Started
 
 You can use docker compose for development and testing out the application, there are several services that compose Immich:
 
@@ -217,6 +217,17 @@ You can get the app on F-droid by clicking the image below.
   <img src="design/ios-qr-code.png" width="200" title="Apple App Store">
 <p/>
 
+
+# Development
+
+The development environment can be start from root of the project after populating the `.env` file with the command
+
+```bash
+make dev # required Makefile installed on the system.
+``` 
+
+All servers and web container are hot reload for quick feedback loop.
+
 # Support
 
 If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsore**](https://github.com/sponsors/alextran1502), or one time donation with Buy Me a coffee link below.

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 594 - 16
server/package-lock.json


+ 4 - 0
server/package.json

@@ -33,6 +33,7 @@
     "@nestjs/platform-express": "^8.0.0",
     "@nestjs/platform-fastify": "^8.2.6",
     "@nestjs/platform-socket.io": "^8.2.6",
+    "@nestjs/schedule": "^2.0.1",
     "@nestjs/typeorm": "^8.0.3",
     "@nestjs/websockets": "^8.2.6",
     "@socket.io/redis-adapter": "^7.1.0",
@@ -53,6 +54,7 @@
     "reflect-metadata": "^0.1.13",
     "rimraf": "^3.0.2",
     "rxjs": "^7.2.0",
+    "sharp": "^0.30.4",
     "socket.io-redis": "^6.1.1",
     "systeminformation": "^5.11.0",
     "typeorm": "^0.2.41"
@@ -63,6 +65,7 @@
     "@nestjs/testing": "^8.0.0",
     "@types/bcrypt": "^5.0.0",
     "@types/bull": "^3.15.7",
+    "@types/cron": "^2.0.0",
     "@types/express": "^4.17.13",
     "@types/imagemin": "^8.0.0",
     "@types/jest": "27.0.2",
@@ -70,6 +73,7 @@
     "@types/multer": "^1.4.7",
     "@types/node": "^16.0.0",
     "@types/passport-jwt": "^3.0.6",
+    "@types/sharp": "^0.30.2",
     "@types/supertest": "^2.0.11",
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",

+ 10 - 2
server/src/api-v1/asset/asset.service.ts

@@ -114,7 +114,11 @@ export class AssetService {
   public async getAssetThumbnail(assetId: string) {
     const asset = await this.assetRepository.findOne({ id: assetId });
 
-    return new StreamableFile(createReadStream(asset.resizePath));
+    if (asset.webpPath != '') {
+      return new StreamableFile(createReadStream(asset.webpPath));
+    } else {
+      return new StreamableFile(createReadStream(asset.resizePath));
+    }
   }
 
   public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
@@ -132,7 +136,11 @@ export class AssetService {
       if (query.isThumb === 'false' || !query.isThumb) {
         file = createReadStream(asset.originalPath);
       } else {
-        file = createReadStream(asset.resizePath);
+        if (asset.webpPath != '') {
+          file = createReadStream(asset.webpPath);
+        } else {
+          file = createReadStream(asset.resizePath);
+        }
       }
 
       file.on('error', (error) => {

+ 3 - 0
server/src/api-v1/asset/entities/asset.entity.ts

@@ -26,6 +26,9 @@ export class AssetEntity {
   @Column({ nullable: true })
   resizePath: string;
 
+  @Column({ nullable: true })
+  webpPath: string;
+
   @Column()
   createdAt: string;
 

+ 14 - 0
server/src/app.module.ts

@@ -16,16 +16,26 @@ import { BackgroundTaskModule } from './modules/background-task/background-task.
 import { CommunicationModule } from './api-v1/communication/communication.module';
 import { SharingModule } from './api-v1/sharing/sharing.module';
 import { AppController } from './app.controller';
+import { ScheduleModule } from '@nestjs/schedule';
+import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module';
+
 
 @Module({
   imports: [
     ConfigModule.forRoot(immichAppConfig),
+
     TypeOrmModule.forRoot(databaseConfig),
+
     UserModule,
+
     AssetModule,
+
     AuthModule,
+
     ImmichJwtModule,
+
     DeviceInfoModule,
+
     BullModule.forRootAsync({
       useFactory: async () => ({
         redis: {
@@ -44,6 +54,10 @@ import { AppController } from './app.controller';
     CommunicationModule,
 
     SharingModule,
+
+    ScheduleModule.forRoot(),
+
+    ScheduleTasksModule
   ],
   controllers: [AppController],
   providers: [],

+ 1 - 0
server/src/config/app.config.ts

@@ -17,5 +17,6 @@ export const immichAppConfig: ConfigModuleOptions = {
       then: Joi.string().optional().allow(null, ''),
       otherwise: Joi.string().required(),
     }),
+    VITE_SERVER_ENDPOINT: Joi.string().required(),
   }),
 };

+ 2 - 2
server/src/constants/server_version.constant.ts

@@ -3,7 +3,7 @@
 
 export const serverVersion = {
   major: 1,
-  minor: 9,
+  minor: 10,
   patch: 0,
-  build: 13,
+  build: 14,
 };

+ 19 - 0
server/src/migration/1653214255670-UpdateAssetTableWithWebpPath.ts

@@ -0,0 +1,19 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class UpdateAssetTableWithWebpPath1653214255670 implements MigrationInterface {
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`
+      alter table assets
+        add column if not exists "webpPath" varchar default '';
+      `)
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`
+      alter table assets
+        drop column if exists "webpPath";     
+      `);
+  }
+
+}

+ 52 - 0
server/src/modules/schedule-tasks/image-conversion.service.ts

@@ -0,0 +1,52 @@
+import { Injectable, Logger } from '@nestjs/common';
+import { Cron, CronExpression } from '@nestjs/schedule';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
+import sharp from 'sharp';
+
+@Injectable()
+export class ImageConversionService {
+
+  constructor(
+    @InjectRepository(AssetEntity)
+    private assetRepository: Repository<AssetEntity>
+  ) { }
+
+  @Cron(CronExpression.EVERY_5_MINUTES
+    , {
+      name: 'webp-conversion'
+    })
+  async webpConversion() {
+    Logger.log('Starting Webp Conversion Tasks', 'ImageConversionService')
+
+    const assets = await this.assetRepository.find({
+      where: {
+        webpPath: ''
+      },
+      take: 500
+    });
+
+
+    if (assets.length == 0) {
+      Logger.log('All assets has webp file - aborting task', 'ImageConversionService')
+      return;
+    }
+
+
+    for (const asset of assets) {
+      const resizePath = asset.resizePath;
+      if (resizePath != '') {
+        const webpPath = resizePath.replace('jpeg', 'webp')
+
+        sharp(resizePath).resize(250).webp().toFile(webpPath, (err, info) => {
+
+          if (!err) {
+            this.assetRepository.update({ id: asset.id }, { webpPath: webpPath })
+          }
+
+        });
+      }
+    }
+  }
+}

+ 12 - 0
server/src/modules/schedule-tasks/schedule-tasks.module.ts

@@ -0,0 +1,12 @@
+import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
+import { ImageConversionService } from './image-conversion.service';
+
+@Module({
+  imports: [
+    TypeOrmModule.forFeature([AssetEntity]),
+  ],
+  providers: [ImageConversionService],
+})
+export class ScheduleTasksModule { }

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.