mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00: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,7 +63,6 @@ services:
 | 
			
		||||
      - immich-network
 | 
			
		||||
    restart: always
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  redis:
 | 
			
		||||
    container_name: immich_redis
 | 
			
		||||
    image: redis:6.2
 | 
			
		||||
@@ -65,7 +70,6 @@ services:
 | 
			
		||||
      - immich-network
 | 
			
		||||
    restart: always
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  database:
 | 
			
		||||
    container_name: immich_postgres
 | 
			
		||||
    image: postgres:14
 | 
			
		||||
@@ -82,6 +86,7 @@ services:
 | 
			
		||||
      - 5432:5432
 | 
			
		||||
    networks:
 | 
			
		||||
      - immich-network
 | 
			
		||||
    restart: always
 | 
			
		||||
 | 
			
		||||
  nginx:
 | 
			
		||||
    container_name: proxy_nginx
 | 
			
		||||
 
 | 
			
		||||
@@ -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,7 +63,6 @@ services:
 | 
			
		||||
      - immich-network
 | 
			
		||||
    restart: always
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  redis:
 | 
			
		||||
    container_name: immich_redis
 | 
			
		||||
    image: redis:6.2
 | 
			
		||||
@@ -55,7 +70,6 @@ services:
 | 
			
		||||
      - immich-network
 | 
			
		||||
    restart: always
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  database:
 | 
			
		||||
    container_name: immich_postgres
 | 
			
		||||
    image: postgres:14
 | 
			
		||||
 
 | 
			
		||||
@@ -33,3 +33,5 @@ lerna-debug.log*
 | 
			
		||||
!.vscode/tasks.json
 | 
			
		||||
!.vscode/launch.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.
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
        var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
 | 
			
		||||
 | 
			
		||||
        if (isAuthenticated) {
 | 
			
		||||
          ref.watch(backupProvider.notifier).resumeBackup();
 | 
			
		||||
        ref.watch(websocketProvider.notifier).connect();
 | 
			
		||||
          ref.watch(assetProvider.notifier).getAllAsset();
 | 
			
		||||
          ref.watch(serverInfoProvider.notifier).getServerVersion();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ref.watch(websocketProvider.notifier).connect();
 | 
			
		||||
 | 
			
		||||
        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());
 | 
			
		||||
    } 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,6 +106,7 @@ export class AssetService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async downloadFile(query: ServeFileDto, res: Res) {
 | 
			
		||||
    try {
 | 
			
		||||
      let file = null;
 | 
			
		||||
      const asset = await this.findOne(query.did, query.aid);
 | 
			
		||||
 | 
			
		||||
@@ -121,16 +127,25 @@ export class AssetService {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return new StreamableFile(file);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      Logger.error('Error download asset ', e);
 | 
			
		||||
      throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getAssetThumbnail(assetId: string) {
 | 
			
		||||
    try {
 | 
			
		||||
      const asset = await this.assetRepository.findOne({ id: assetId });
 | 
			
		||||
 | 
			
		||||
    if (asset.webpPath != '') {
 | 
			
		||||
      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');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
 | 
			
		||||
@@ -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,7 +168,7 @@ export class AssetService {
 | 
			
		||||
        return new StreamableFile(createReadStream(asset.resizePath));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        /**
 | 
			
		||||
         * Serve thumbnail image for both web and mobile app
 | 
			
		||||
         */
 | 
			
		||||
@@ -164,10 +178,11 @@ export class AssetService {
 | 
			
		||||
          });
 | 
			
		||||
          file = createReadStream(asset.originalPath);
 | 
			
		||||
        } else {
 | 
			
		||||
        if (asset.webpPath != '') {
 | 
			
		||||
          if (asset.webpPath && asset.webpPath.length > 0) {
 | 
			
		||||
            res.set({
 | 
			
		||||
              'Content-Type': 'image/webp',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            file = createReadStream(asset.webpPath);
 | 
			
		||||
          } else {
 | 
			
		||||
            res.set({
 | 
			
		||||
@@ -183,8 +198,12 @@ export class AssetService {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
@@ -232,20 +251,20 @@ export class AssetService {
 | 
			
		||||
            '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