You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	WIP refactor container and queuing system (#206)
* refactor microservices to machine-learning * Update tGithub issue template with correct task syntax * Added microservices container * Communicate between service based on queue system * added dependency * Fixed problem with having to import BullQueue into the individual service * Added todo * refactor server into monorepo with microservices * refactor database and entity to library * added simple migration * Move migrations and database config to library * Migration works in library * Cosmetic change in logging message * added user dto * Fixed issue with testing not able to find the shared library * Clean up library mapping path * Added webp generator to microservices * Update Github Action build latest * Fixed issue NPM cannot install due to conflict witl Bull Queue * format project with prettier * Modified docker-compose file * Add GH Action for Staging build: * Fixed GH action job name * Modified GH Action to only build & push latest when pushing to main * Added Test 2e2 Github Action * Added Test 2e2 Github Action * Implemented microservice to extract exif * Added cronjob to scan and generate webp thumbnail at midnight * Refactor to ireduce hit time to database when running microservices * Added error handling to asset services that handle read file from disk * Added video transcoding queue to process one video at a time * Fixed loading spinner on web while loading covering the info panel * Add mechanism to show new release announcement to web and mobile app (#209) * Added changelog page * Fixed issues based on PR comments * Fixed issue with video transcoding run on the server * Change entry point content for backward combatibility when starting up server * Added announcement box * Added error handling to failed silently when the app version checking is not able to make the request to GITHUB * Added new version announcement overlay * Update message * Added messages * Added logic to check and show announcement * Add method to handle saving new version * Added button to dimiss the acknowledge message * Up version for deployment to the app store
This commit is contained in:
		
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							| @@ -16,10 +16,10 @@ Note: Please search to see if an issue already exists for the bug you encountere | ||||
| A clear and concise description of what the bug is. | ||||
|  | ||||
| **Task List** | ||||
| [ ] I have read thoroughly the README setup and installation instructions. | ||||
| [ ] If my setup is different, I have included my docker-compose file. | ||||
| [ ] I have included my redacted `.env` file. | ||||
| [ ] I have included information on my machine, and environment. | ||||
| - [ ] I have read thoroughly the README setup and installation instructions. | ||||
| - [ ] If my setup is different, I have included my docker-compose file. | ||||
| - [ ] I have included my redacted `.env` file. | ||||
| - [ ] I have included information on my machine, and environment. | ||||
|  | ||||
| **To Reproduce** | ||||
| Steps to reproduce the behavior: | ||||
|   | ||||
							
								
								
									
										27
									
								
								.github/workflows/build_push_docker_latest.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/build_push_docker_latest.yml
									
									
									
									
										vendored
									
									
								
							| @@ -4,17 +4,16 @@ on: | ||||
|   workflow_dispatch: | ||||
|   push: | ||||
|     branches: [main] | ||||
|   pull_request: | ||||
|     branches: [main] | ||||
|  | ||||
| jobs: | ||||
|   build_and_push_server_latest: | ||||
|   # This image include both the server and microservices - the two containers can be slitted into separated | ||||
|   # service with its coressponding entry file. | ||||
|   build_and_push_server_monorepo_latest: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           # ref: "main" # branch | ||||
|           fetch-depth: 0 | ||||
|  | ||||
|       - name: Set up QEMU | ||||
| @@ -27,23 +26,22 @@ jobs: | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|       - name: Build and push Immich | ||||
|       - name: Build and push Immich Mono Repo | ||||
|         uses: docker/build-push-action@v3.0.0 | ||||
|         with: | ||||
|           context: ./server | ||||
|           file: ./server/Dockerfile | ||||
|           platforms: linux/arm/v7,linux/amd64,linux/arm64 | ||||
|           push: ${{ github.event_name != 'pull_request' }} | ||||
|           push: true | ||||
|           tags: | | ||||
|             altran1502/immich-server:latest | ||||
|  | ||||
|   build_and_push_microservice_latest: | ||||
|   build_and_push_machine_learning_latest: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           # ref: "main" # branch | ||||
|           fetch-depth: 0 | ||||
|  | ||||
|       - name: Set up QEMU | ||||
| @@ -56,15 +54,15 @@ jobs: | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|       - name: Build and Push Microservices | ||||
|       - name: Build and Push Machine Learning | ||||
|         uses: docker/build-push-action@v3.0.0 | ||||
|         with: | ||||
|           context: ./microservices | ||||
|           file: ./microservices/Dockerfile | ||||
|           context: ./machine-learning | ||||
|           file: ./machine-learning/Dockerfile | ||||
|           platforms: linux/arm/v7,linux/amd64 | ||||
|           push: ${{ github.event_name != 'pull_request' }} | ||||
|           push: true | ||||
|           tags: | | ||||
|             altran1502/immich-microservices:latest | ||||
|             altran1502/immich-machine-learning:latest | ||||
|  | ||||
|   build_and_push_web_latest: | ||||
|     runs-on: ubuntu-latest | ||||
| @@ -72,7 +70,6 @@ jobs: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           # ref: "main" # branch | ||||
|           fetch-depth: 0 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2.0.0 | ||||
| @@ -91,6 +88,6 @@ jobs: | ||||
|           file: ./web/Dockerfile | ||||
|           platforms: linux/arm/v7,linux/amd64,linux/arm64 | ||||
|           target: prod | ||||
|           push: ${{ github.event_name != 'pull_request' }} | ||||
|           push: true | ||||
|           tags: | | ||||
|             altran1502/immich-web:latest | ||||
|   | ||||
							
								
								
									
										95
									
								
								.github/workflows/build_push_docker_staging.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								.github/workflows/build_push_docker_staging.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| name: Build and Push Docker Image - Staging | ||||
|  | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   push: | ||||
|     branches: [main] | ||||
|   pull_request: | ||||
|     branches: [main] | ||||
|  | ||||
| jobs: | ||||
|   # This image include both the server and microservices - the two containers can be slitted into separated | ||||
|   # service with its coressponding entry file. | ||||
|   build_and_push_server_monorepo_staging: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|  | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2.0.0 | ||||
|       - name: Set up Docker Buildx | ||||
|         id: buildx | ||||
|         uses: docker/setup-buildx-action@v2.0.0 | ||||
|       - name: Login to Docker Hub | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|       - name: Build and push Immich Mono Repo | ||||
|         uses: docker/build-push-action@v3.0.0 | ||||
|         with: | ||||
|           context: ./server | ||||
|           file: ./server/Dockerfile | ||||
|           platforms: linux/arm/v7,linux/amd64,linux/arm64 | ||||
|           push: ${{ github.event_name == 'pull_request' }} | ||||
|           tags: | | ||||
|             altran1502/immich-server:staging | ||||
|  | ||||
|   build_and_push_machine_learning_staging: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|  | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2.0.0 | ||||
|       - name: Set up Docker Buildx | ||||
|         id: buildx | ||||
|         uses: docker/setup-buildx-action@v2.0.0 | ||||
|       - name: Login to Docker Hub | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|       - name: Build and Push Machine Learning | ||||
|         uses: docker/build-push-action@v3.0.0 | ||||
|         with: | ||||
|           context: ./machine-learning | ||||
|           file: ./machine-learning/Dockerfile | ||||
|           platforms: linux/arm/v7,linux/amd64 | ||||
|           push: ${{ github.event_name == 'pull_request' }} | ||||
|           tags: | | ||||
|             altran1502/immich-machine-learning:staging | ||||
|  | ||||
|   build_and_push_web_staging: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2.0.0 | ||||
|       - name: Set up Docker Buildx | ||||
|         id: buildx | ||||
|         uses: docker/setup-buildx-action@v2.0.0 | ||||
|       - name: Login to Docker Hub | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|       - name: Build and Push Web | ||||
|         uses: docker/build-push-action@v3.0.0 | ||||
|         with: | ||||
|           context: ./web | ||||
|           file: ./web/Dockerfile | ||||
|           platforms: linux/arm/v7,linux/amd64,linux/arm64 | ||||
|           target: prod | ||||
|           push: ${{ github.event_name == 'pull_request' }} | ||||
|           tags: | | ||||
|             altran1502/immich-web:staging | ||||
| @@ -6,7 +6,7 @@ services: | ||||
|     build: | ||||
|       context: ../server | ||||
|       dockerfile: Dockerfile | ||||
|     command: npm run start:dev | ||||
|     command: npm run start:dev immich | ||||
|     expose: | ||||
|       - "3000" | ||||
|     volumes: | ||||
| @@ -23,16 +23,35 @@ services: | ||||
|     networks: | ||||
|       - immich-network | ||||
|  | ||||
|   immich-microservices: | ||||
|     image: immich-microservices-dev:1.9.0 | ||||
|   immich-machine-learning: | ||||
|     image: immich-machine-learning-dev:1.9.0 | ||||
|     build: | ||||
|       context: ../microservices | ||||
|       context: ../machine-learning | ||||
|       dockerfile: Dockerfile | ||||
|     command: npm run start:dev | ||||
|     expose: | ||||
|       - "3001" | ||||
|     volumes: | ||||
|       - ../microservices:/usr/src/app | ||||
|       - ../machine-learning:/usr/src/app | ||||
|       - ${UPLOAD_LOCATION}:/usr/src/app/upload | ||||
|       - /usr/src/app/node_modules | ||||
|     env_file: | ||||
|       - .env | ||||
|     environment: | ||||
|       - NODE_ENV=development | ||||
|     depends_on: | ||||
|       - database | ||||
|     networks: | ||||
|       - immich-network | ||||
|  | ||||
|   immich-microservices: | ||||
|     image: immich-microservices:1.9.0 | ||||
|     build: | ||||
|       context: ../server | ||||
|       dockerfile: Dockerfile | ||||
|     command: npm run start:dev microservices | ||||
|     volumes: | ||||
|       - ../server:/usr/src/app | ||||
|       - ${UPLOAD_LOCATION}:/usr/src/app/upload | ||||
|       - /usr/src/app/node_modules | ||||
|     env_file: | ||||
|   | ||||
| @@ -2,11 +2,8 @@ version: "3.8" | ||||
|  | ||||
| services: | ||||
|   immich-server: | ||||
|     image: immich-server-staging:latest | ||||
|     build: | ||||
|       context: ../server | ||||
|       dockerfile: Dockerfile | ||||
|     entrypoint: ["/bin/sh", "./entrypoint.sh"] | ||||
|     image: altran1502/immich-server:staging | ||||
|     entrypoint: ["/bin/sh", "./start-server.sh"] | ||||
|     expose: | ||||
|       - "3000" | ||||
|     volumes: | ||||
| @@ -23,10 +20,23 @@ services: | ||||
|     restart: always | ||||
|  | ||||
|   immich-microservices: | ||||
|     image: immich-microservices-staging:latest | ||||
|     build: | ||||
|       context: ../microservices | ||||
|       dockerfile: Dockerfile | ||||
|     image: altran1502/immich-server:staging | ||||
|     entrypoint: ["/bin/sh", "./start-microservices.sh"] | ||||
|     volumes: | ||||
|       - ${UPLOAD_LOCATION}:/usr/src/app/upload | ||||
|     env_file: | ||||
|       - .env | ||||
|     environment: | ||||
|       - NODE_ENV=production | ||||
|     depends_on: | ||||
|       - redis | ||||
|       - database | ||||
|     networks: | ||||
|       - immich-network | ||||
|     restart: always | ||||
|  | ||||
|   immich-machine-learning: | ||||
|     image: altran1502/immich-machine-learning:staging | ||||
|     entrypoint: ["/bin/sh", "./entrypoint.sh"] | ||||
|     expose: | ||||
|       - "3001" | ||||
| @@ -43,12 +53,8 @@ services: | ||||
|     restart: always | ||||
|  | ||||
|   immich-web: | ||||
|     image: immich-web-staging:latest | ||||
|     image: altran1502/immich-web:staging | ||||
|     entrypoint: ["/bin/sh", "./entrypoint.sh"] | ||||
|     build: | ||||
|       context: ../web | ||||
|       dockerfile: Dockerfile | ||||
|       target: prod | ||||
|     env_file: | ||||
|       - .env | ||||
|     ports: | ||||
| @@ -57,14 +63,12 @@ services: | ||||
|       - immich-network | ||||
|     restart: always | ||||
|  | ||||
|  | ||||
|   redis: | ||||
|     container_name: immich_redis | ||||
|     image: redis:6.2 | ||||
|     networks: | ||||
|       - immich-network | ||||
|     restart: always | ||||
|      | ||||
|  | ||||
|   database: | ||||
|     container_name: immich_postgres | ||||
| @@ -82,6 +86,7 @@ services: | ||||
|       - 5432:5432 | ||||
|     networks: | ||||
|       - immich-network | ||||
|     restart: always | ||||
|  | ||||
|   nginx: | ||||
|     container_name: proxy_nginx | ||||
| @@ -102,4 +107,4 @@ services: | ||||
| networks: | ||||
|   immich-network: | ||||
| volumes: | ||||
|   pgdata: | ||||
|   pgdata: | ||||
|   | ||||
| @@ -3,7 +3,7 @@ version: "3.8" | ||||
| services: | ||||
|   immich-server: | ||||
|     image: altran1502/immich-server:latest | ||||
|     entrypoint: ["/bin/sh", "./entrypoint.sh"] | ||||
|     entrypoint: ["/bin/sh", "./start-server.sh"] | ||||
|     expose: | ||||
|       - "3000" | ||||
|     volumes: | ||||
| @@ -20,7 +20,23 @@ services: | ||||
|     restart: always | ||||
|  | ||||
|   immich-microservices: | ||||
|     image: altran1502/immich-microservices:latest | ||||
|     image: altran1502/immich-server:latest | ||||
|     entrypoint: ["/bin/sh", "./start-microservices.sh"] | ||||
|     volumes: | ||||
|       - ${UPLOAD_LOCATION}:/usr/src/app/upload | ||||
|     env_file: | ||||
|       - .env | ||||
|     environment: | ||||
|       - NODE_ENV=production | ||||
|     depends_on: | ||||
|       - redis | ||||
|       - database | ||||
|     networks: | ||||
|       - immich-network | ||||
|     restart: always | ||||
|  | ||||
|   immich-machine-learning: | ||||
|     image: altran1502/immich-machine-learning:latest | ||||
|     entrypoint: ["/bin/sh", "./entrypoint.sh"] | ||||
|     expose: | ||||
|       - "3001" | ||||
| @@ -47,14 +63,12 @@ services: | ||||
|       - immich-network | ||||
|     restart: always | ||||
|  | ||||
|  | ||||
|   redis: | ||||
|     container_name: immich_redis | ||||
|     image: redis:6.2 | ||||
|     networks: | ||||
|       - immich-network | ||||
|     restart: always | ||||
|      | ||||
|  | ||||
|   database: | ||||
|     container_name: immich_postgres | ||||
| @@ -73,7 +87,7 @@ services: | ||||
|     networks: | ||||
|       - immich-network | ||||
|     restart: always | ||||
|      | ||||
|  | ||||
|   nginx: | ||||
|     container_name: proxy_nginx | ||||
|     image: nginx:latest | ||||
| @@ -93,4 +107,4 @@ services: | ||||
| networks: | ||||
|   immich-network: | ||||
| volumes: | ||||
|   pgdata: | ||||
|   pgdata: | ||||
|   | ||||
| @@ -32,4 +32,6 @@ lerna-debug.log* | ||||
| !.vscode/settings.json | ||||
| !.vscode/tasks.json | ||||
| !.vscode/launch.json | ||||
| !.vscode/extensions.json | ||||
| !.vscode/extensions.json | ||||
| 
 | ||||
| upload/ | ||||
| @@ -7,7 +7,7 @@ export class ImageClassifierController { | ||||
|     private readonly imageClassifierService: ImageClassifierService, | ||||
|   ) { } | ||||
| 
 | ||||
|   @Post('/tagImage') | ||||
|   @Post('/tag-image') | ||||
|   async tagImage(@Body('thumbnailPath') thumbnailPath: string) { | ||||
|     return await this.imageClassifierService.tagImage(thumbnailPath); | ||||
|   } | ||||
| @@ -8,14 +8,14 @@ async function bootstrap() { | ||||
|   await app.listen(3001, () => { | ||||
|     if (process.env.NODE_ENV == 'development') { | ||||
|       Logger.log( | ||||
|         'Running Immich Microservices in DEVELOPMENT environment', | ||||
|         'Running Immich Machine Learning in DEVELOPMENT environment', | ||||
|         'IMMICH MICROSERVICES', | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (process.env.NODE_ENV == 'production') { | ||||
|       Logger.log( | ||||
|         'Running Immich Microservices in PRODUCTION environment', | ||||
|         'Running Immich Machine Learning in PRODUCTION environment', | ||||
|         'IMMICH MICROSERVICES', | ||||
|       ); | ||||
|     } | ||||
| @@ -8,7 +8,7 @@ export class ObjectDetectionController { | ||||
|     private readonly objectDetectionService: ObjectDetectionService, | ||||
|   ) { } | ||||
| 
 | ||||
|   @Post('/detectObject') | ||||
|   @Post('/detect-object') | ||||
|   async detectObject(@Body('thumbnailPath') thumbnailPath: string) { | ||||
|     return await this.objectDetectionService.detectObject(thumbnailPath); | ||||
|   } | ||||
| @@ -1 +0,0 @@ | ||||
| devenv/ | ||||
							
								
								
									
										3
									
								
								machine_learning/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								machine_learning/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +0,0 @@ | ||||
| __pycache__/ | ||||
| devenv/ | ||||
| app/upload | ||||
| @@ -1,25 +0,0 @@ | ||||
| ## GPU Build | ||||
| # FROM tensorflow/tensorflow:latest-gpu as gpu | ||||
|  | ||||
| # WORKDIR /code | ||||
|  | ||||
| # COPY ./requirements.txt /code/requirements.txt | ||||
|  | ||||
| # RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt | ||||
|  | ||||
| # COPY ./app /code/app | ||||
|  | ||||
|  | ||||
| ## CPU BUILD | ||||
| FROM python:3.8 as cpu | ||||
|  | ||||
| RUN apt-get update | ||||
| RUN apt-get install ffmpeg libsm6 libxext6  -y | ||||
|  | ||||
| WORKDIR /code | ||||
|  | ||||
| COPY ./requirements.txt /code/requirements.txt | ||||
|  | ||||
| RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt | ||||
|  | ||||
| COPY ./app /code/app | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 193 KiB | 
| @@ -1,37 +0,0 @@ | ||||
| from tensorflow.keras.applications import InceptionV3 | ||||
| from tensorflow.keras.applications.inception_v3 import preprocess_input, decode_predictions | ||||
| from tensorflow.keras.preprocessing import image | ||||
| import numpy as np | ||||
| from PIL import Image | ||||
| import cv2 | ||||
| IMG_SIZE = 299 | ||||
| PREDICTION_MODEL = InceptionV3(weights='imagenet') | ||||
|  | ||||
|  | ||||
| def classify_image(image_path: str): | ||||
|     img_path = f'./app/{image_path}' | ||||
|     # img = image.load_img(img_path, target_size=(IMG_SIZE, IMG_SIZE)) | ||||
|  | ||||
|     target_image = cv2.imread(img_path, cv2.IMREAD_UNCHANGED) | ||||
|     resized_target_image = cv2.resize(target_image, (IMG_SIZE, IMG_SIZE)) | ||||
|  | ||||
|     x = image.img_to_array(resized_target_image) | ||||
|     x = np.expand_dims(x, axis=0) | ||||
|     x = preprocess_input(x) | ||||
|  | ||||
|     preds = PREDICTION_MODEL.predict(x) | ||||
|     result = decode_predictions(preds, top=3)[0] | ||||
|     payload = [] | ||||
|     for _, value, _ in result: | ||||
|         payload.append(value) | ||||
|  | ||||
|     return payload | ||||
|  | ||||
|  | ||||
| def warm_up(): | ||||
|     img_path = f'./app/test.png' | ||||
|     img = image.load_img(img_path, target_size=(IMG_SIZE, IMG_SIZE)) | ||||
|     x = image.img_to_array(img) | ||||
|     x = np.expand_dims(x, axis=0) | ||||
|     x = preprocess_input(x) | ||||
|     PREDICTION_MODEL.predict(x) | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,46 +0,0 @@ | ||||
| from pydantic import BaseModel | ||||
| from fastapi import FastAPI | ||||
|  | ||||
| from .object_detection import object_detection | ||||
| from .image_classifier import image_classifier | ||||
|  | ||||
| from tf2_yolov4.anchors import YOLOV4_ANCHORS | ||||
| from tf2_yolov4.model import YOLOv4 | ||||
|  | ||||
|  | ||||
| HEIGHT, WIDTH = (640, 960) | ||||
|  | ||||
| # Warm up model | ||||
| image_classifier.warm_up() | ||||
| app = FastAPI() | ||||
|  | ||||
|  | ||||
| class TagImagePayload(BaseModel): | ||||
|     thumbnail_path: str | ||||
|  | ||||
|  | ||||
| @app.post("/tagImage") | ||||
| async def post_root(payload: TagImagePayload): | ||||
|     image_path = payload.thumbnail_path | ||||
|  | ||||
|     if image_path[0] == '.': | ||||
|         image_path = image_path[2:] | ||||
|  | ||||
|     return image_classifier.classify_image(image_path=image_path) | ||||
|  | ||||
|  | ||||
| @app.get("/") | ||||
| async def test(): | ||||
|  | ||||
|     object_detection.run_detection() | ||||
|     # image = tf.io.read_file("./app/cars.jpg") | ||||
|     # image = tf.image.decode_image(image) | ||||
|     # image = tf.image.resize(image, (HEIGHT, WIDTH)) | ||||
|     # images = tf.expand_dims(image, axis=0) / 255.0 | ||||
|  | ||||
|     # model = YOLOv4( | ||||
|     #     (HEIGHT, WIDTH, 3), | ||||
|     #     80, | ||||
|     #     YOLOV4_ANCHORS, | ||||
|     #     "darknet", | ||||
|     # ) | ||||
| @@ -1,4 +0,0 @@ | ||||
|  | ||||
|  | ||||
| def run_detection(): | ||||
|     print("run detection") | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 345 KiB | 
| @@ -1,8 +0,0 @@ | ||||
| opencv-python==4.5.5.64 | ||||
| fastapi>=0.68.0,<0.69.0 | ||||
| pydantic>=1.8.0,<2.0.0 | ||||
| uvicorn>=0.15.0,<0.16.0 | ||||
| tensorflow==2.8.0 | ||||
| numpy==1.22.2 | ||||
| pillow==9.0.1 | ||||
| tf2_yolov4==0.1.0 | ||||
| @@ -23,4 +23,11 @@ | ||||
|   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||
|   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||
|   <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> | ||||
|  | ||||
|   <queries> | ||||
|     <intent> | ||||
|       <action android:name="android.intent.action.VIEW" /> | ||||
|       <data android:scheme="https" /> | ||||
|     </intent> | ||||
|   </queries> | ||||
| </manifest> | ||||
| @@ -0,0 +1 @@ | ||||
| * Added announcement pop-up when a new released is pushed out in Github. | ||||
| @@ -58,7 +58,7 @@ | ||||
|       <string>UIInterfaceOrientationPortrait</string> | ||||
|       <string>UIInterfaceOrientationLandscapeLeft</string> | ||||
|       <string>UIInterfaceOrientationLandscapeRight</string> | ||||
|     </array> | ||||
|   </array> | ||||
|     <key>UISupportedInterfaceOrientations~ipad</key> | ||||
|     <array> | ||||
|       <string>UIInterfaceOrientationPortrait</string> | ||||
| @@ -76,5 +76,11 @@ | ||||
|     <false /> | ||||
|     <key>CADisableMinimumFrameDurationOnPhone</key> | ||||
|     <true /> | ||||
|  | ||||
|  | ||||
|     <key>LSApplicationQueriesSchemes</key> | ||||
|     <array> | ||||
|       <string>https</string> | ||||
|     </array> | ||||
|   </dict> | ||||
| </plist> | ||||
| @@ -19,7 +19,7 @@ platform :ios do | ||||
|   desc "iOS Beta" | ||||
|   lane :beta do | ||||
|     increment_version_number( | ||||
|       version_number: "1.10.1" | ||||
|       version_number: "1.11.0" | ||||
|     ) | ||||
|     increment_build_number( | ||||
|       build_number: latest_testflight_build_number + 1, | ||||
|   | ||||
| @@ -13,3 +13,7 @@ const String savedLoginInfoKey = "immichSavedLoginInfoKey"; | ||||
| // Backup Info | ||||
| const String hiveBackupInfoBox = "immichBackupAlbumInfoBox"; | ||||
| const String backupInfoKey = "immichBackupAlbumInfoKey"; | ||||
|  | ||||
| // Github Release Info | ||||
| const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; | ||||
| const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; | ||||
|   | ||||
| @@ -5,14 +5,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/immich_colors.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/asset.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/routing/tab_navigation_observer.dart'; | ||||
| import 'package:immich_mobile/shared/providers/app_state.provider.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/release_info.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/server_info.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/websocket.provider.dart'; | ||||
| import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; | ||||
| import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; | ||||
| import 'constants/hive_box.dart'; | ||||
|  | ||||
| void main() async { | ||||
| @@ -24,6 +27,7 @@ void main() async { | ||||
|   await Hive.openBox(userInfoBox); | ||||
|   await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox); | ||||
|   await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox); | ||||
|   await Hive.openBox(hiveGithubReleaseInfoBox); | ||||
|  | ||||
|   SystemChrome.setSystemUIOverlayStyle( | ||||
|     const SystemUiOverlayStyle( | ||||
| @@ -48,10 +52,18 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv | ||||
|       case AppLifecycleState.resumed: | ||||
|         debugPrint("[APP STATE] resumed"); | ||||
|         ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed; | ||||
|         ref.watch(backupProvider.notifier).resumeBackup(); | ||||
|  | ||||
|         var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated; | ||||
|  | ||||
|         if (isAuthenticated) { | ||||
|           ref.watch(backupProvider.notifier).resumeBackup(); | ||||
|           ref.watch(assetProvider.notifier).getAllAsset(); | ||||
|           ref.watch(serverInfoProvider.notifier).getServerVersion(); | ||||
|         } | ||||
|  | ||||
|         ref.watch(websocketProvider.notifier).connect(); | ||||
|         ref.watch(assetProvider.notifier).getAllAsset(); | ||||
|         ref.watch(serverInfoProvider.notifier).getServerVersion(); | ||||
|  | ||||
|         ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); | ||||
|  | ||||
|         break; | ||||
|  | ||||
| @@ -95,6 +107,8 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); | ||||
|  | ||||
|     return MaterialApp( | ||||
|       debugShowCheckedModeBanner: false, | ||||
|       home: Stack( | ||||
| @@ -121,6 +135,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv | ||||
|             routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]), | ||||
|           ), | ||||
|           const ImmichLoadingOverlay(), | ||||
|           const VersionAnnouncementOverlay(), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
							
								
								
									
										57
									
								
								mobile/lib/shared/providers/release_info.provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								mobile/lib/shared/providers/release_info.provider.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; | ||||
|  | ||||
| class ReleaseInfoNotifier extends StateNotifier<String> { | ||||
|   ReleaseInfoNotifier() : super(""); | ||||
|  | ||||
|   void checkGithubReleaseInfo() async { | ||||
|     var dio = Dio(); | ||||
|     var box = Hive.box(hiveGithubReleaseInfoBox); | ||||
|  | ||||
|     try { | ||||
|       String? localReleaseVersion = box.get(githubReleaseInfoKey); | ||||
|  | ||||
|       Response res = await dio.get( | ||||
|         "https://api.github.com/repos/alextran1502/immich/releases/latest", | ||||
|         options: Options( | ||||
|           headers: {"Accept": "application/vnd.github.v3+json"}, | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       if (res.statusCode == 200) { | ||||
|         String latestTagVersion = res.data["tag_name"]; | ||||
|         state = latestTagVersion; | ||||
|  | ||||
|         debugPrint("Local release version $localReleaseVersion"); | ||||
|         debugPrint("Remote release veresion $latestTagVersion"); | ||||
|  | ||||
|         if (localReleaseVersion == null && latestTagVersion.isNotEmpty) { | ||||
|           VersionAnnouncementOverlayController.appLoader.show(); | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         if (latestTagVersion.isNotEmpty && localReleaseVersion != latestTagVersion) { | ||||
|           VersionAnnouncementOverlayController.appLoader.show(); | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       debugPrint("Error gettting latest release version"); | ||||
|  | ||||
|       state = ""; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void acknowledgeNewVersion() { | ||||
|     var box = Hive.box(hiveGithubReleaseInfoBox); | ||||
|  | ||||
|     box.put(githubReleaseInfoKey, state); | ||||
|     VersionAnnouncementOverlayController.appLoader.hide(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>((ref) => ReleaseInfoNotifier()); | ||||
| @@ -19,11 +19,6 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> { | ||||
|  | ||||
|   final ServerInfoService _serverInfoService = ServerInfoService(); | ||||
|  | ||||
|   getMapboxInfo() async { | ||||
|     MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo(); | ||||
|     state = state.copyWith(mapboxInfo: mapboxInfoRes); | ||||
|   } | ||||
|  | ||||
|   getServerVersion() async { | ||||
|     ServerVersion? serverVersion = await _serverInfoService.getServerVersion(); | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:immich_mobile/shared/models/mapbox_info.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/server_version.model.dart'; | ||||
| import 'package:immich_mobile/shared/services/network.service.dart'; | ||||
| @@ -13,15 +14,16 @@ class ServerInfoService { | ||||
|     return ServerInfo.fromJson(response.toString()); | ||||
|   } | ||||
|  | ||||
|   Future<MapboxInfo> getMapboxInfo() async { | ||||
|     Response response = await _networkService.getRequest(url: 'server-info/mapbox'); | ||||
|  | ||||
|     return MapboxInfo.fromJson(response.toString()); | ||||
|   } | ||||
|  | ||||
|   Future<ServerVersion?> getServerVersion() async { | ||||
|     Response response = await _networkService.getRequest(url: 'server-info/version'); | ||||
|     try { | ||||
|       Response response = | ||||
|           await _networkService.getRequest(url: 'server-info/version'); | ||||
|  | ||||
|     return ServerVersion.fromJson(response.toString()); | ||||
|       return ServerVersion.fromJson(response.toString()); | ||||
|     } catch (e) { | ||||
|       debugPrint("Error getting server info"); | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										133
									
								
								mobile/lib/shared/views/version_announcement_overlay.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								mobile/lib/shared/views/version_announcement_overlay.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| import 'package:flutter/gestures.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/shared/providers/release_info.provider.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
|  | ||||
| class VersionAnnouncementOverlay extends HookConsumerWidget { | ||||
|   const VersionAnnouncementOverlay({ | ||||
|     Key? key, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     void goToReleaseNote() async { | ||||
|       final Uri _url = Uri.parse('https://github.com/alextran1502/immich/releases/latest'); | ||||
|       await launchUrl(_url); | ||||
|     } | ||||
|  | ||||
|     void onAcknowledgeTapped() { | ||||
|       ref.watch(releaseInfoProvider.notifier).acknowledgeNewVersion(); | ||||
|     } | ||||
|  | ||||
|     return ValueListenableBuilder<bool>( | ||||
|       valueListenable: VersionAnnouncementOverlayController.appLoader.loaderShowingNotifier, | ||||
|       builder: (context, shouldShow, child) { | ||||
|         if (shouldShow) { | ||||
|           return Scaffold( | ||||
|             backgroundColor: Colors.black38, | ||||
|             body: Center( | ||||
|               child: ConstrainedBox( | ||||
|                 constraints: const BoxConstraints(maxWidth: 307), | ||||
|                 child: Wrap( | ||||
|                   children: [ | ||||
|                     Card( | ||||
|                       child: Padding( | ||||
|                         padding: const EdgeInsets.all(30.0), | ||||
|                         child: Column( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                           children: [ | ||||
|                             const Text( | ||||
|                               "New Server Version Available 🎉", | ||||
|                               style: TextStyle( | ||||
|                                 fontSize: 16, | ||||
|                                 fontFamily: 'WorkSans', | ||||
|                                 fontWeight: FontWeight.bold, | ||||
|                                 color: Colors.indigo, | ||||
|                               ), | ||||
|                             ), | ||||
|                             Padding( | ||||
|                               padding: const EdgeInsets.only(top: 16.0), | ||||
|                               child: RichText( | ||||
|                                 text: TextSpan( | ||||
|                                   style: const TextStyle( | ||||
|                                       fontSize: 14, fontFamily: 'WorkSans', color: Colors.black87, height: 1.2), | ||||
|                                   children: <TextSpan>[ | ||||
|                                     const TextSpan( | ||||
|                                       text: 'Hi friend, there is a new release of', | ||||
|                                     ), | ||||
|                                     const TextSpan( | ||||
|                                       text: ' Immich ', | ||||
|                                       style: TextStyle( | ||||
|                                         fontFamily: "SnowBurstOne", | ||||
|                                         color: Colors.indigo, | ||||
|                                         fontWeight: FontWeight.bold, | ||||
|                                       ), | ||||
|                                     ), | ||||
|                                     const TextSpan( | ||||
|                                       text: "please take your time to visit the ", | ||||
|                                     ), | ||||
|                                     TextSpan( | ||||
|                                       text: "release note", | ||||
|                                       style: const TextStyle( | ||||
|                                         decoration: TextDecoration.underline, | ||||
|                                       ), | ||||
|                                       recognizer: TapGestureRecognizer()..onTap = goToReleaseNote, | ||||
|                                     ), | ||||
|                                     const TextSpan( | ||||
|                                       text: | ||||
|                                           " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", | ||||
|                                     ) | ||||
|                                   ], | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ), | ||||
|                             Padding( | ||||
|                               padding: const EdgeInsets.only(top: 16.0), | ||||
|                               child: ElevatedButton( | ||||
|                                   style: ElevatedButton.styleFrom( | ||||
|                                     shape: const StadiumBorder(), | ||||
|                                     visualDensity: VisualDensity.standard, | ||||
|                                     primary: Colors.indigo, | ||||
|                                     onPrimary: Colors.grey[50], | ||||
|                                     elevation: 2, | ||||
|                                     padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), | ||||
|                                   ), | ||||
|                                   onPressed: onAcknowledgeTapped, | ||||
|                                   child: const Text( | ||||
|                                     "Acknowledge", | ||||
|                                     style: TextStyle( | ||||
|                                       fontSize: 14, | ||||
|                                     ), | ||||
|                                   )), | ||||
|                             ) | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ); | ||||
|         } else { | ||||
|           return Container(); | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class VersionAnnouncementOverlayController { | ||||
|   static final VersionAnnouncementOverlayController appLoader = VersionAnnouncementOverlayController(); | ||||
|   ValueNotifier<bool> loaderShowingNotifier = ValueNotifier(false); | ||||
|   ValueNotifier<String> loaderTextNotifier = ValueNotifier('error message'); | ||||
|  | ||||
|   void show() { | ||||
|     loaderShowingNotifier.value = true; | ||||
|   } | ||||
|  | ||||
|   void hide() { | ||||
|     loaderShowingNotifier.value = false; | ||||
|   } | ||||
| } | ||||
| @@ -1015,6 +1015,62 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.4" | ||||
|   url_launcher: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: url_launcher | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "6.1.3" | ||||
|   url_launcher_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_android | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "6.0.17" | ||||
|   url_launcher_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_ios | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "6.0.17" | ||||
|   url_launcher_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_linux | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|   url_launcher_macos: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_macos | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|   url_launcher_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_platform_interface | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.5" | ||||
|   url_launcher_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_web | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.11" | ||||
|   url_launcher_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_windows | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|   uuid: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -2,7 +2,7 @@ name: immich_mobile | ||||
| description: Immich - selfhosted backup media file on mobile phone | ||||
|  | ||||
| publish_to: "none" | ||||
| version: 1.10.1+16 | ||||
| version: 1.11.0+17 | ||||
|  | ||||
| environment: | ||||
|   sdk: ">=2.15.1 <3.0.0" | ||||
| @@ -39,6 +39,7 @@ dependencies: | ||||
|   flutter_swipe_detector: ^2.0.0 | ||||
|   equatable: ^2.0.3 | ||||
|   image_picker: ^0.8.5+3 | ||||
|   url_launcher: ^6.1.3 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| FROM node:16-alpine3.14 | ||||
| FROM node:16-alpine3.14 as core | ||||
|  | ||||
| ARG DEBIAN_FRONTEND=noninteractive | ||||
|  | ||||
|   | ||||
| @@ -23,7 +23,7 @@ import { assetUploadOption } from '../../config/asset-upload.config'; | ||||
| import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; | ||||
| import { CreateAssetDto } from './dto/create-asset.dto'; | ||||
| import { ServeFileDto } from './dto/serve-file.dto'; | ||||
| import { AssetEntity } from './entities/asset.entity'; | ||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||
| import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; | ||||
| import { Response as Res } from 'express'; | ||||
| import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto'; | ||||
| @@ -31,6 +31,8 @@ import { BackgroundTaskService } from '../../modules/background-task/background- | ||||
| import { DeleteAssetDto } from './dto/delete-asset.dto'; | ||||
| import { SearchAssetDto } from './dto/search-asset.dto'; | ||||
| import { CommunicationGateway } from '../communication/communication.gateway'; | ||||
| import { InjectQueue } from '@nestjs/bull'; | ||||
| import { Queue } from 'bull'; | ||||
| 
 | ||||
| @UseGuards(JwtAuthGuard) | ||||
| @Controller('asset') | ||||
| @@ -39,7 +41,10 @@ export class AssetController { | ||||
|     private wsCommunicateionGateway: CommunicationGateway, | ||||
|     private assetService: AssetService, | ||||
|     private backgroundTaskService: BackgroundTaskService, | ||||
|   ) { } | ||||
| 
 | ||||
|     @InjectQueue('asset-uploaded-queue') | ||||
|     private assetUploadedQueue: Queue, | ||||
|   ) {} | ||||
| 
 | ||||
|   @Post('upload') | ||||
|   @UseInterceptors( | ||||
| @@ -61,12 +66,23 @@ export class AssetController { | ||||
|         const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype); | ||||
| 
 | ||||
|         if (uploadFiles.thumbnailData != null && savedAsset) { | ||||
|           await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path); | ||||
|           await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset); | ||||
|           await this.backgroundTaskService.detectObject(uploadFiles.thumbnailData[0].path, savedAsset); | ||||
|         } | ||||
|           const assetWithThumbnail = await this.assetService.updateThumbnailInfo( | ||||
|             savedAsset, | ||||
|             uploadFiles.thumbnailData[0].path, | ||||
|           ); | ||||
| 
 | ||||
|         await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size); | ||||
|           await this.assetUploadedQueue.add( | ||||
|             'asset-uploaded', | ||||
|             { asset: assetWithThumbnail, fileName: file.originalname, fileSize: file.size, hasThumbnail: true }, | ||||
|             { jobId: savedAsset.id }, | ||||
|           ); | ||||
|         } else { | ||||
|           await this.assetUploadedQueue.add( | ||||
|             'asset-uploaded', | ||||
|             { asset: savedAsset, fileName: file.originalname, fileSize: file.size, hasThumbnail: false }, | ||||
|             { jobId: savedAsset.id }, | ||||
|           ); | ||||
|         } | ||||
| 
 | ||||
|         this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset)); | ||||
|       } catch (e) { | ||||
| @@ -2,9 +2,7 @@ import { Module } from '@nestjs/common'; | ||||
| import { AssetService } from './asset.service'; | ||||
| import { AssetController } from './asset.controller'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { AssetEntity } from './entities/asset.entity'; | ||||
| import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module'; | ||||
| import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service'; | ||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||
| import { BullModule } from '@nestjs/bull'; | ||||
| import { BackgroundTaskModule } from '../../modules/background-task/background-task.module'; | ||||
| import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; | ||||
| @@ -13,29 +11,19 @@ import { CommunicationModule } from '../communication/communication.module'; | ||||
| @Module({ | ||||
|   imports: [ | ||||
|     CommunicationModule, | ||||
| 
 | ||||
|     BullModule.registerQueue({ | ||||
|       name: 'optimize', | ||||
|       defaultJobOptions: { | ||||
|         attempts: 3, | ||||
|         removeOnComplete: true, | ||||
|         removeOnFail: false, | ||||
|       }, | ||||
|     }), | ||||
|     BullModule.registerQueue({ | ||||
|       name: 'background-task', | ||||
|       defaultJobOptions: { | ||||
|         attempts: 3, | ||||
|         removeOnComplete: true, | ||||
|         removeOnFail: false, | ||||
|       }, | ||||
|     }), | ||||
|     TypeOrmModule.forFeature([AssetEntity]), | ||||
|     ImageOptimizeModule, | ||||
|     BackgroundTaskModule, | ||||
|     TypeOrmModule.forFeature([AssetEntity]), | ||||
|     BullModule.registerQueue({ | ||||
|       name: 'asset-uploaded-queue', | ||||
|       defaultJobOptions: { | ||||
|         attempts: 3, | ||||
|         removeOnComplete: true, | ||||
|         removeOnFail: false, | ||||
|       }, | ||||
|     }), | ||||
|   ], | ||||
|   controllers: [AssetController], | ||||
|   providers: [AssetService, AssetOptimizeService, BackgroundTaskService], | ||||
|   providers: [AssetService, BackgroundTaskService], | ||||
|   exports: [], | ||||
| }) | ||||
| export class AssetModule { } | ||||
| export class AssetModule {} | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common'; | ||||
| import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import { Repository } from 'typeorm'; | ||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||
| import { CreateAssetDto } from './dto/create-asset.dto'; | ||||
| import { AssetEntity, AssetType } from './entities/asset.entity'; | ||||
| import { AssetEntity, AssetType } from '@app/database/entities/asset.entity'; | ||||
| import _ from 'lodash'; | ||||
| import { createReadStream, stat } from 'fs'; | ||||
| import { ServeFileDto } from './dto/serve-file.dto'; | ||||
| @@ -11,7 +11,6 @@ import { Response as Res } from 'express'; | ||||
| import { promisify } from 'util'; | ||||
| import { DeleteAssetDto } from './dto/delete-asset.dto'; | ||||
| import { SearchAssetDto } from './dto/search-asset.dto'; | ||||
| import ffmpeg from 'fluent-ffmpeg'; | ||||
| 
 | ||||
| const fileInfo = promisify(stat); | ||||
| 
 | ||||
| @@ -20,12 +19,18 @@ export class AssetService { | ||||
|   constructor( | ||||
|     @InjectRepository(AssetEntity) | ||||
|     private assetRepository: Repository<AssetEntity>, | ||||
|   ) { } | ||||
|   ) {} | ||||
| 
 | ||||
|   public async updateThumbnailInfo(assetId: string, path: string) { | ||||
|     return await this.assetRepository.update(assetId, { | ||||
|       resizePath: path, | ||||
|     }); | ||||
|   public async updateThumbnailInfo(asset: AssetEntity, thumbnailPath: string): Promise<AssetEntity> { | ||||
|     const updatedAsset = await this.assetRepository | ||||
|       .createQueryBuilder('assets') | ||||
|       .update<AssetEntity>(AssetEntity, { ...asset, resizePath: thumbnailPath }) | ||||
|       .where('assets.id = :id', { id: asset.id }) | ||||
|       .returning('*') | ||||
|       .updateEntity(true) | ||||
|       .execute(); | ||||
| 
 | ||||
|     return updatedAsset.raw[0]; | ||||
|   } | ||||
| 
 | ||||
|   public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) { | ||||
| @@ -66,13 +71,13 @@ export class AssetService { | ||||
|     try { | ||||
|       return await this.assetRepository.find({ | ||||
|         where: { | ||||
|           userId: authUser.id | ||||
|           userId: authUser.id, | ||||
|         }, | ||||
|         relations: ['exifInfo'], | ||||
|         order: { | ||||
|           createdAt: 'DESC' | ||||
|         } | ||||
|       }) | ||||
|           createdAt: 'DESC', | ||||
|         }, | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       Logger.error(e, 'getAllAssets'); | ||||
|     } | ||||
| @@ -101,35 +106,45 @@ export class AssetService { | ||||
|   } | ||||
| 
 | ||||
|   public async downloadFile(query: ServeFileDto, res: Res) { | ||||
|     let file = null; | ||||
|     const asset = await this.findOne(query.did, query.aid); | ||||
|     try { | ||||
|       let file = null; | ||||
|       const asset = await this.findOne(query.did, query.aid); | ||||
| 
 | ||||
|     if (query.isThumb === 'false' || !query.isThumb) { | ||||
|       const { size } = await fileInfo(asset.originalPath); | ||||
|       res.set({ | ||||
|         'Content-Type': asset.mimeType, | ||||
|         'Content-Length': size, | ||||
|       }); | ||||
|       file = createReadStream(asset.originalPath); | ||||
|     } else { | ||||
|       const { size } = await fileInfo(asset.resizePath); | ||||
|       res.set({ | ||||
|         'Content-Type': 'image/jpeg', | ||||
|         'Content-Length': size, | ||||
|       }); | ||||
|       file = createReadStream(asset.resizePath); | ||||
|       if (query.isThumb === 'false' || !query.isThumb) { | ||||
|         const { size } = await fileInfo(asset.originalPath); | ||||
|         res.set({ | ||||
|           'Content-Type': asset.mimeType, | ||||
|           'Content-Length': size, | ||||
|         }); | ||||
|         file = createReadStream(asset.originalPath); | ||||
|       } else { | ||||
|         const { size } = await fileInfo(asset.resizePath); | ||||
|         res.set({ | ||||
|           'Content-Type': 'image/jpeg', | ||||
|           'Content-Length': size, | ||||
|         }); | ||||
|         file = createReadStream(asset.resizePath); | ||||
|       } | ||||
| 
 | ||||
|       return new StreamableFile(file); | ||||
|     } catch (e) { | ||||
|       Logger.error('Error download asset ', e); | ||||
|       throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile'); | ||||
|     } | ||||
| 
 | ||||
|     return new StreamableFile(file); | ||||
|   } | ||||
| 
 | ||||
|   public async getAssetThumbnail(assetId: string) { | ||||
|     const asset = await this.assetRepository.findOne({ id: assetId }); | ||||
|     try { | ||||
|       const asset = await this.assetRepository.findOne({ id: assetId }); | ||||
| 
 | ||||
|     if (asset.webpPath != '') { | ||||
|       return new StreamableFile(createReadStream(asset.webpPath)); | ||||
|     } else { | ||||
|       return new StreamableFile(createReadStream(asset.resizePath)); | ||||
|       if (asset.webpPath && asset.webpPath.length > 0) { | ||||
|         return new StreamableFile(createReadStream(asset.webpPath)); | ||||
|       } else { | ||||
|         return new StreamableFile(createReadStream(asset.resizePath)); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       Logger.error('Error serving asset thumbnail ', e); | ||||
|       throw new InternalServerErrorException('Failed to serve asset thumbnail', 'GetAssetThumbnail'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @@ -141,7 +156,6 @@ export class AssetService { | ||||
|       throw new BadRequestException('Asset does not exist'); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     // Handle Sending Images
 | ||||
|     if (asset.type == AssetType.IMAGE || query.isThumb == 'true') { | ||||
|       /** | ||||
| @@ -154,97 +168,102 @@ export class AssetService { | ||||
|         return new StreamableFile(createReadStream(asset.resizePath)); | ||||
|       } | ||||
| 
 | ||||
| 
 | ||||
|       /** | ||||
|        * Serve thumbnail image for both web and mobile app | ||||
|        */ | ||||
|       if (query.isThumb === 'false' || !query.isThumb) { | ||||
|         res.set({ | ||||
|           'Content-Type': asset.mimeType, | ||||
|         }); | ||||
|         file = createReadStream(asset.originalPath); | ||||
|       } else { | ||||
|         if (asset.webpPath != '') { | ||||
|       try { | ||||
|         /** | ||||
|          * Serve thumbnail image for both web and mobile app | ||||
|          */ | ||||
|         if (query.isThumb === 'false' || !query.isThumb) { | ||||
|           res.set({ | ||||
|             'Content-Type': 'image/webp', | ||||
|             'Content-Type': asset.mimeType, | ||||
|           }); | ||||
|           file = createReadStream(asset.webpPath); | ||||
|           file = createReadStream(asset.originalPath); | ||||
|         } else { | ||||
|           if (asset.webpPath && asset.webpPath.length > 0) { | ||||
|             res.set({ | ||||
|               'Content-Type': 'image/webp', | ||||
|             }); | ||||
| 
 | ||||
|             file = createReadStream(asset.webpPath); | ||||
|           } else { | ||||
|             res.set({ | ||||
|               'Content-Type': 'image/jpeg', | ||||
|             }); | ||||
|             file = createReadStream(asset.resizePath); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         file.on('error', (error) => { | ||||
|           Logger.log(`Cannot create read stream ${error}`); | ||||
|           return new BadRequestException('Cannot Create Read Stream'); | ||||
|         }); | ||||
| 
 | ||||
|         return new StreamableFile(file); | ||||
|       } catch (e) { | ||||
|         Logger.error('Error serving IMAGE asset ', e); | ||||
|         throw new InternalServerErrorException(`Failed to serve image asset ${e}`, 'ServeFile'); | ||||
|       } | ||||
|     } else if (asset.type == AssetType.VIDEO) { | ||||
|       try { | ||||
|         // Handle Video
 | ||||
|         let videoPath = asset.originalPath; | ||||
|         let mimeType = asset.mimeType; | ||||
| 
 | ||||
|         if (query.isWeb && asset.mimeType == 'video/quicktime') { | ||||
|           videoPath = asset.encodedVideoPath == '' ? asset.originalPath : asset.encodedVideoPath; | ||||
|           mimeType = asset.encodedVideoPath == '' ? asset.mimeType : 'video/mp4'; | ||||
|         } | ||||
| 
 | ||||
|         const { size } = await fileInfo(videoPath); | ||||
|         const range = headers.range; | ||||
| 
 | ||||
|         if (range) { | ||||
|           /** Extracting Start and End value from Range Header */ | ||||
|           let [start, end] = range.replace(/bytes=/, '').split('-'); | ||||
|           start = parseInt(start, 10); | ||||
|           end = end ? parseInt(end, 10) : size - 1; | ||||
| 
 | ||||
|           if (!isNaN(start) && isNaN(end)) { | ||||
|             start = start; | ||||
|             end = size - 1; | ||||
|           } | ||||
|           if (isNaN(start) && !isNaN(end)) { | ||||
|             start = size - end; | ||||
|             end = size - 1; | ||||
|           } | ||||
| 
 | ||||
|           // Handle unavailable range request
 | ||||
|           if (start >= size || end >= size) { | ||||
|             console.error('Bad Request'); | ||||
|             // Return the 416 Range Not Satisfiable.
 | ||||
|             res.status(416).set({ | ||||
|               'Content-Range': `bytes */${size}`, | ||||
|             }); | ||||
| 
 | ||||
|             throw new BadRequestException('Bad Request Range'); | ||||
|           } | ||||
| 
 | ||||
|           /** Sending Partial Content With HTTP Code 206 */ | ||||
| 
 | ||||
|           res.status(206).set({ | ||||
|             'Content-Range': `bytes ${start}-${end}/${size}`, | ||||
|             'Accept-Ranges': 'bytes', | ||||
|             'Content-Length': end - start + 1, | ||||
|             'Content-Type': mimeType, | ||||
|           }); | ||||
| 
 | ||||
|           const videoStream = createReadStream(videoPath, { start: start, end: end }); | ||||
| 
 | ||||
|           return new StreamableFile(videoStream); | ||||
|         } else { | ||||
|           res.set({ | ||||
|             'Content-Type': 'image/jpeg', | ||||
|           }); | ||||
|           file = createReadStream(asset.resizePath); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       file.on('error', (error) => { | ||||
|         Logger.log(`Cannot create read stream ${error}`); | ||||
|         return new BadRequestException('Cannot Create Read Stream'); | ||||
|       }); | ||||
| 
 | ||||
|       return new StreamableFile(file); | ||||
| 
 | ||||
|     } else if (asset.type == AssetType.VIDEO) { | ||||
|       // Handle Video
 | ||||
|       let videoPath = asset.originalPath; | ||||
|       let mimeType = asset.mimeType; | ||||
| 
 | ||||
|       if (query.isWeb && asset.mimeType == 'video/quicktime') { | ||||
|         videoPath = asset.encodedVideoPath == '' ? asset.originalPath : asset.encodedVideoPath; | ||||
|         mimeType = asset.encodedVideoPath == '' ? asset.mimeType : 'video/mp4'; | ||||
|       } | ||||
| 
 | ||||
|       const { size } = await fileInfo(videoPath); | ||||
|       const range = headers.range; | ||||
| 
 | ||||
|       if (range) { | ||||
|         /** Extracting Start and End value from Range Header */ | ||||
|         let [start, end] = range.replace(/bytes=/, '').split('-'); | ||||
|         start = parseInt(start, 10); | ||||
|         end = end ? parseInt(end, 10) : size - 1; | ||||
| 
 | ||||
|         if (!isNaN(start) && isNaN(end)) { | ||||
|           start = start; | ||||
|           end = size - 1; | ||||
|         } | ||||
|         if (isNaN(start) && !isNaN(end)) { | ||||
|           start = size - end; | ||||
|           end = size - 1; | ||||
|         } | ||||
| 
 | ||||
|         // Handle unavailable range request
 | ||||
|         if (start >= size || end >= size) { | ||||
|           console.error('Bad Request'); | ||||
|           // Return the 416 Range Not Satisfiable.
 | ||||
|           res.status(416).set({ | ||||
|             'Content-Range': `bytes */${size}`, | ||||
|             'Content-Type': mimeType, | ||||
|           }); | ||||
| 
 | ||||
|           throw new BadRequestException('Bad Request Range'); | ||||
|           return new StreamableFile(createReadStream(videoPath)); | ||||
|         } | ||||
| 
 | ||||
|         /** Sending Partial Content With HTTP Code 206 */ | ||||
| 
 | ||||
|         res.status(206).set({ | ||||
|           'Content-Range': `bytes ${start}-${end}/${size}`, | ||||
|           'Accept-Ranges': 'bytes', | ||||
|           'Content-Length': end - start + 1, | ||||
|           'Content-Type': mimeType, | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|         const videoStream = createReadStream(videoPath, { start: start, end: end }); | ||||
| 
 | ||||
|         return new StreamableFile(videoStream); | ||||
| 
 | ||||
| 
 | ||||
|       } else { | ||||
| 
 | ||||
|         res.set({ | ||||
|           'Content-Type': mimeType, | ||||
|         }); | ||||
| 
 | ||||
|         return new StreamableFile(createReadStream(videoPath)); | ||||
|       } catch (e) { | ||||
|         Logger.error('Error serving VIDEO asset ', e); | ||||
|         throw new InternalServerErrorException(`Failed to serve video asset ${e}`, 'ServeFile'); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { IsNotEmpty, IsOptional } from 'class-validator'; | ||||
| import { AssetType } from '../entities/asset.entity'; | ||||
| import { AssetType } from '@app/database/entities/asset.entity'; | ||||
| 
 | ||||
| export class CreateAssetDto { | ||||
|   @IsNotEmpty() | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { AssetEntity } from '../entities/asset.entity'; | ||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||
| 
 | ||||
| export class GetAllAssetReponseDto { | ||||
|   data: Array<{ date: string; assets: Array<AssetEntity> }>; | ||||
| @@ -7,7 +7,7 @@ import { SignUpDto } from './dto/sign-up.dto'; | ||||
| 
 | ||||
| @Controller('auth') | ||||
| export class AuthController { | ||||
|   constructor(private readonly authService: AuthService) { } | ||||
|   constructor(private readonly authService: AuthService) {} | ||||
| 
 | ||||
|   @Post('/login') | ||||
|   async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto) { | ||||
| @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; | ||||
| import { AuthService } from './auth.service'; | ||||
| import { AuthController } from './auth.controller'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { UserEntity } from '../user/entities/user.entity'; | ||||
| import { UserEntity } from '@app/database/entities/user.entity'; | ||||
| import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; | ||||
| import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; | ||||
| import { JwtModule } from '@nestjs/jwt'; | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { BadRequestException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import { Repository } from 'typeorm'; | ||||
| import { UserEntity } from '../user/entities/user.entity'; | ||||
| import { UserEntity } from '@app/database/entities/user.entity'; | ||||
| import { LoginCredentialDto } from './dto/login-credential.dto'; | ||||
| import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; | ||||
| import { JwtPayloadDto } from './dto/jwt-payload.dto'; | ||||
| @@ -4,7 +4,7 @@ import { Socket, Server } from 'socket.io'; | ||||
| import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; | ||||
| import { Logger } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import { UserEntity } from '../user/entities/user.entity'; | ||||
| import { UserEntity } from '@app/database/entities/user.entity'; | ||||
| import { Repository } from 'typeorm'; | ||||
| 
 | ||||
| @WebSocketGateway() | ||||
| @@ -6,7 +6,7 @@ import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; | ||||
| import { JwtModule } from '@nestjs/jwt'; | ||||
| import { jwtConfig } from '../../config/jwt.config'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { UserEntity } from '../user/entities/user.entity'; | ||||
| import { UserEntity } from '@app/database/entities/user.entity'; | ||||
| 
 | ||||
| @Module({ | ||||
|   imports: [TypeOrmModule.forFeature([UserEntity]), ImmichJwtModule, JwtModule.register(jwtConfig)], | ||||
| @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; | ||||
| import { DeviceInfoService } from './device-info.service'; | ||||
| import { DeviceInfoController } from './device-info.controller'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { DeviceInfoEntity } from './entities/device-info.entity'; | ||||
| import { DeviceInfoEntity } from '@app/database/entities/device-info.entity'; | ||||
| 
 | ||||
| @Module({ | ||||
|   imports: [TypeOrmModule.forFeature([DeviceInfoEntity])], | ||||
| @@ -4,7 +4,7 @@ import { Repository } from 'typeorm'; | ||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||
| import { CreateDeviceInfoDto } from './dto/create-device-info.dto'; | ||||
| import { UpdateDeviceInfoDto } from './dto/update-device-info.dto'; | ||||
| import { DeviceInfoEntity } from './entities/device-info.entity'; | ||||
| import { DeviceInfoEntity } from '@app/database/entities/device-info.entity'; | ||||
| 
 | ||||
| @Injectable() | ||||
| export class DeviceInfoService { | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { IsNotEmpty, IsOptional } from 'class-validator'; | ||||
| import { DeviceType } from '../entities/device-info.entity'; | ||||
| import { DeviceType } from '@app/database/entities/device-info.entity'; | ||||
| 
 | ||||
| export class CreateDeviceInfoDto { | ||||
|   @IsNotEmpty() | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { PartialType } from '@nestjs/mapped-types'; | ||||
| import { IsOptional } from 'class-validator'; | ||||
| import { DeviceType } from '../entities/device-info.entity'; | ||||
| import { DeviceType } from '@app/database/entities/device-info.entity'; | ||||
| import { CreateDeviceInfoDto } from './create-device-info.dto'; | ||||
| 
 | ||||
| export class UpdateDeviceInfoDto extends PartialType(CreateDeviceInfoDto) {} | ||||
| @@ -4,6 +4,6 @@ import { ServerInfoController } from './server-info.controller'; | ||||
| 
 | ||||
| @Module({ | ||||
|   controllers: [ServerInfoController], | ||||
|   providers: [ServerInfoService] | ||||
|   providers: [ServerInfoService], | ||||
| }) | ||||
| export class ServerInfoModule {} | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { IsNotEmpty } from 'class-validator'; | ||||
| import { AssetEntity } from '../../asset/entities/asset.entity'; | ||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||
| 
 | ||||
| export class AddAssetsDto { | ||||
|   @IsNotEmpty() | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { IsNotEmpty, IsOptional } from 'class-validator'; | ||||
| import { AssetEntity } from '../../asset/entities/asset.entity'; | ||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||
| 
 | ||||
| export class CreateSharedAlbumDto { | ||||
|   @IsNotEmpty() | ||||
| @@ -2,11 +2,11 @@ import { Module } from '@nestjs/common'; | ||||
| import { SharingService } from './sharing.service'; | ||||
| import { SharingController } from './sharing.controller'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { AssetEntity } from '../asset/entities/asset.entity'; | ||||
| import { UserEntity } from '../user/entities/user.entity'; | ||||
| import { SharedAlbumEntity } from './entities/shared-album.entity'; | ||||
| import { AssetSharedAlbumEntity } from './entities/asset-shared-album.entity'; | ||||
| import { UserSharedAlbumEntity } from './entities/user-shared-album.entity'; | ||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||
| import { UserEntity } from '@app/database/entities/user.entity'; | ||||
| import { AssetSharedAlbumEntity } from '@app/database/entities/asset-shared-album.entity'; | ||||
| import { SharedAlbumEntity } from '@app/database/entities/shared-album.entity'; | ||||
| import { UserSharedAlbumEntity } from '@app/database/entities/user-shared-album.entity'; | ||||
| 
 | ||||
| @Module({ | ||||
|   imports: [ | ||||
| @@ -2,13 +2,13 @@ import { BadRequestException, Injectable, NotFoundException, UnauthorizedExcepti | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import { getConnection, Repository } from 'typeorm'; | ||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||
| import { AssetEntity } from '../asset/entities/asset.entity'; | ||||
| import { UserEntity } from '../user/entities/user.entity'; | ||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||
| import { UserEntity } from '@app/database/entities/user.entity'; | ||||
| import { AddAssetsDto } from './dto/add-assets.dto'; | ||||
| import { CreateSharedAlbumDto } from './dto/create-shared-album.dto'; | ||||
| import { AssetSharedAlbumEntity } from './entities/asset-shared-album.entity'; | ||||
| import { SharedAlbumEntity } from './entities/shared-album.entity'; | ||||
| import { UserSharedAlbumEntity } from './entities/user-shared-album.entity'; | ||||
| import { AssetSharedAlbumEntity } from '@app/database/entities/asset-shared-album.entity'; | ||||
| import { SharedAlbumEntity } from '@app/database/entities/shared-album.entity'; | ||||
| import { UserSharedAlbumEntity } from '@app/database/entities/user-shared-album.entity'; | ||||
| import _ from 'lodash'; | ||||
| import { AddUsersDto } from './dto/add-users.dto'; | ||||
| import { RemoveAssetsDto } from './dto/remove-assets.dto'; | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { UserEntity } from '../entities/user.entity'; | ||||
| import { UserEntity } from '../../../../../../libs/database/src/entities/user.entity'; | ||||
| 
 | ||||
| export interface User { | ||||
|   id: string; | ||||
| @@ -1,4 +1,19 @@ | ||||
| import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, ValidationPipe, Put, Query, UseInterceptors, UploadedFile, Response } from '@nestjs/common'; | ||||
| import { | ||||
|   Controller, | ||||
|   Get, | ||||
|   Post, | ||||
|   Body, | ||||
|   Patch, | ||||
|   Param, | ||||
|   Delete, | ||||
|   UseGuards, | ||||
|   ValidationPipe, | ||||
|   Put, | ||||
|   Query, | ||||
|   UseInterceptors, | ||||
|   UploadedFile, | ||||
|   Response, | ||||
| } from '@nestjs/common'; | ||||
| import { UserService } from './user.service'; | ||||
| import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; | ||||
| import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; | ||||
| @@ -11,7 +26,7 @@ import { Response as Res } from 'express'; | ||||
| 
 | ||||
| @Controller('user') | ||||
| export class UserController { | ||||
|   constructor(private readonly userService: UserService) { } | ||||
|   constructor(private readonly userService: UserService) {} | ||||
| 
 | ||||
|   @UseGuards(JwtAuthGuard) | ||||
|   @Get() | ||||
| @@ -28,14 +43,13 @@ export class UserController { | ||||
| 
 | ||||
|   @Get('/count') | ||||
|   async getUserCount(@Query('isAdmin') isAdmin: boolean) { | ||||
| 
 | ||||
|     return await this.userService.getUserCount(isAdmin); | ||||
|   } | ||||
| 
 | ||||
|   @UseGuards(JwtAuthGuard) | ||||
|   @Put() | ||||
|   async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto) { | ||||
|     return await this.userService.updateUser(updateUserDto) | ||||
|     return await this.userService.updateUser(updateUserDto); | ||||
|   } | ||||
| 
 | ||||
|   @UseGuards(JwtAuthGuard) | ||||
| @@ -46,9 +60,7 @@ export class UserController { | ||||
|   } | ||||
| 
 | ||||
|   @Get('/profile-image/:userId') | ||||
|   async getProfileImage(@Param('userId') userId: string, | ||||
|     @Response({ passthrough: true }) res: Res, | ||||
|   ) { | ||||
|   async getProfileImage(@Param('userId') userId: string, @Response({ passthrough: true }) res: Res) { | ||||
|     return await this.userService.getUserProfileImage(userId, res); | ||||
|   } | ||||
| } | ||||
| @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; | ||||
| import { UserService } from './user.service'; | ||||
| import { UserController } from './user.controller'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { UserEntity } from './entities/user.entity'; | ||||
| import { UserEntity } from '@app/database/entities/user.entity'; | ||||
| import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; | ||||
| import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; | ||||
| import { JwtModule } from '@nestjs/jwt'; | ||||
| @@ -13,4 +13,4 @@ import { jwtConfig } from '../../config/jwt.config'; | ||||
|   controllers: [UserController], | ||||
|   providers: [UserService, ImmichJwtService], | ||||
| }) | ||||
| export class UserModule { } | ||||
| export class UserModule {} | ||||
| @@ -4,7 +4,7 @@ import { Not, Repository } from 'typeorm'; | ||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||
| import { CreateUserDto } from './dto/create-user.dto'; | ||||
| import { UpdateUserDto } from './dto/update-user.dto'; | ||||
| import { UserEntity } from './entities/user.entity'; | ||||
| import { UserEntity } from '@app/database/entities/user.entity'; | ||||
| import * as bcrypt from 'bcrypt'; | ||||
| import { createReadStream } from 'fs'; | ||||
| import { Response as Res } from 'express'; | ||||
| @@ -1,15 +1,13 @@ | ||||
| import { Controller, Get, Res, Headers } from '@nestjs/common'; | ||||
| import { Response } from 'express'; | ||||
| 
 | ||||
| @Controller() | ||||
| 
 | ||||
| export class AppController { | ||||
|   constructor() { } | ||||
|   constructor() {} | ||||
| 
 | ||||
|   @Get() | ||||
|   async redirectToWebpage(@Res({ passthrough: true }) res: Response, @Headers() headers) { | ||||
|     const host = headers.host; | ||||
| 
 | ||||
|     return res.redirect(`http://${host}:2285`) | ||||
|     return res.redirect(`http://${host}:2285`); | ||||
|   } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user