diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 494abedfeb832..029596b3da206 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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: diff --git a/.github/workflows/build_push_docker_latest.yml b/.github/workflows/build_push_docker_latest.yml index 80ae74bb2dc6e..e436eb60acda2 100644 --- a/.github/workflows/build_push_docker_latest.yml +++ b/.github/workflows/build_push_docker_latest.yml @@ -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 diff --git a/.github/workflows/build_push_docker_staging.yml b/.github/workflows/build_push_docker_staging.yml new file mode 100644 index 0000000000000..1f5347d9b89d9 --- /dev/null +++ b/.github/workflows/build_push_docker_staging.yml @@ -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 diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index a834ee951ea01..1b763ed6c4dc2 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -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: diff --git a/docker/docker-compose.staging.yml b/docker/docker-compose.staging.yml index 0e02e5ad0bd41..55b442d832726 100644 --- a/docker/docker-compose.staging.yml +++ b/docker/docker-compose.staging.yml @@ -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: \ No newline at end of file + pgdata: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8f4e61f41e748..d6e90dd864de3 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -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: \ No newline at end of file + pgdata: diff --git a/microservices/.dockerignore b/machine-learning/.dockerignore similarity index 100% rename from microservices/.dockerignore rename to machine-learning/.dockerignore diff --git a/microservices/.eslintrc.js b/machine-learning/.eslintrc.js similarity index 100% rename from microservices/.eslintrc.js rename to machine-learning/.eslintrc.js diff --git a/microservices/.gitignore b/machine-learning/.gitignore similarity index 91% rename from microservices/.gitignore rename to machine-learning/.gitignore index 22f55adc56472..eccd2d48628f6 100644 --- a/microservices/.gitignore +++ b/machine-learning/.gitignore @@ -32,4 +32,6 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +upload/ \ No newline at end of file diff --git a/microservices/.prettierrc b/machine-learning/.prettierrc similarity index 100% rename from microservices/.prettierrc rename to machine-learning/.prettierrc diff --git a/microservices/Dockerfile b/machine-learning/Dockerfile similarity index 100% rename from microservices/Dockerfile rename to machine-learning/Dockerfile diff --git a/microservices/README.md b/machine-learning/README.md similarity index 100% rename from microservices/README.md rename to machine-learning/README.md diff --git a/microservices/entrypoint.sh b/machine-learning/entrypoint.sh similarity index 100% rename from microservices/entrypoint.sh rename to machine-learning/entrypoint.sh diff --git a/microservices/nest-cli.json b/machine-learning/nest-cli.json similarity index 100% rename from microservices/nest-cli.json rename to machine-learning/nest-cli.json diff --git a/microservices/package-lock.json b/machine-learning/package-lock.json similarity index 100% rename from microservices/package-lock.json rename to machine-learning/package-lock.json diff --git a/microservices/package.json b/machine-learning/package.json similarity index 100% rename from microservices/package.json rename to machine-learning/package.json diff --git a/microservices/src/app.module.ts b/machine-learning/src/app.module.ts similarity index 100% rename from microservices/src/app.module.ts rename to machine-learning/src/app.module.ts diff --git a/microservices/src/config/database.config.ts b/machine-learning/src/config/database.config.ts similarity index 100% rename from microservices/src/config/database.config.ts rename to machine-learning/src/config/database.config.ts diff --git a/microservices/src/image-classifier/image-classifier.controller.ts b/machine-learning/src/image-classifier/image-classifier.controller.ts similarity index 95% rename from microservices/src/image-classifier/image-classifier.controller.ts rename to machine-learning/src/image-classifier/image-classifier.controller.ts index 5dbe8bdffeda9..0cb1655287123 100644 --- a/microservices/src/image-classifier/image-classifier.controller.ts +++ b/machine-learning/src/image-classifier/image-classifier.controller.ts @@ -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); } diff --git a/microservices/src/image-classifier/image-classifier.module.ts b/machine-learning/src/image-classifier/image-classifier.module.ts similarity index 100% rename from microservices/src/image-classifier/image-classifier.module.ts rename to machine-learning/src/image-classifier/image-classifier.module.ts diff --git a/microservices/src/image-classifier/image-classifier.service.ts b/machine-learning/src/image-classifier/image-classifier.service.ts similarity index 100% rename from microservices/src/image-classifier/image-classifier.service.ts rename to machine-learning/src/image-classifier/image-classifier.service.ts diff --git a/microservices/src/main.ts b/machine-learning/src/main.ts similarity index 77% rename from microservices/src/main.ts rename to machine-learning/src/main.ts index 4f9d22d007b1e..5591e76fbdbf1 100644 --- a/microservices/src/main.ts +++ b/machine-learning/src/main.ts @@ -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', ); } diff --git a/microservices/src/object-detection/object-detection.controller.ts b/machine-learning/src/object-detection/object-detection.controller.ts similarity index 94% rename from microservices/src/object-detection/object-detection.controller.ts rename to machine-learning/src/object-detection/object-detection.controller.ts index 77ec167b8d50c..c69c640acb020 100644 --- a/microservices/src/object-detection/object-detection.controller.ts +++ b/machine-learning/src/object-detection/object-detection.controller.ts @@ -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); } diff --git a/microservices/src/object-detection/object-detection.module.ts b/machine-learning/src/object-detection/object-detection.module.ts similarity index 100% rename from microservices/src/object-detection/object-detection.module.ts rename to machine-learning/src/object-detection/object-detection.module.ts diff --git a/microservices/src/object-detection/object-detection.service.ts b/machine-learning/src/object-detection/object-detection.service.ts similarity index 100% rename from microservices/src/object-detection/object-detection.service.ts rename to machine-learning/src/object-detection/object-detection.service.ts diff --git a/microservices/tsconfig.build.json b/machine-learning/tsconfig.build.json similarity index 100% rename from microservices/tsconfig.build.json rename to machine-learning/tsconfig.build.json diff --git a/microservices/tsconfig.json b/machine-learning/tsconfig.json similarity index 100% rename from microservices/tsconfig.json rename to machine-learning/tsconfig.json diff --git a/machine_learning/.dockerignore b/machine_learning/.dockerignore deleted file mode 100644 index 19120dc4356b6..0000000000000 --- a/machine_learning/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -devenv/ \ No newline at end of file diff --git a/machine_learning/.gitignore b/machine_learning/.gitignore deleted file mode 100644 index 530bf8383aa87..0000000000000 --- a/machine_learning/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -__pycache__/ -devenv/ -app/upload \ No newline at end of file diff --git a/machine_learning/Dockerfile b/machine_learning/Dockerfile deleted file mode 100644 index f575b479efba9..0000000000000 --- a/machine_learning/Dockerfile +++ /dev/null @@ -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 \ No newline at end of file diff --git a/machine_learning/app/__init__.py b/machine_learning/app/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/machine_learning/app/cars.jpg b/machine_learning/app/cars.jpg deleted file mode 100644 index 8628adafde7c3..0000000000000 Binary files a/machine_learning/app/cars.jpg and /dev/null differ diff --git a/machine_learning/app/image_classifier/__init__.py b/machine_learning/app/image_classifier/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/machine_learning/app/image_classifier/image_classifier.py b/machine_learning/app/image_classifier/image_classifier.py deleted file mode 100644 index 73fb692c027b7..0000000000000 --- a/machine_learning/app/image_classifier/image_classifier.py +++ /dev/null @@ -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) diff --git a/machine_learning/app/imagenet_class_index.json b/machine_learning/app/imagenet_class_index.json deleted file mode 100644 index fc349e8c3fb05..0000000000000 --- a/machine_learning/app/imagenet_class_index.json +++ /dev/null @@ -1,1002 +0,0 @@ -{ - "0": ["n01440764", "tench"], - "1": ["n01443537", "goldfish"], - "2": ["n01484850", "great_white_shark"], - "3": ["n01491361", "tiger_shark"], - "4": ["n01494475", "hammerhead"], - "5": ["n01496331", "electric_ray"], - "6": ["n01498041", "stingray"], - "7": ["n01514668", "cock"], - "8": ["n01514859", "hen"], - "9": ["n01518878", "ostrich"], - "10": ["n01530575", "brambling"], - "11": ["n01531178", "goldfinch"], - "12": ["n01532829", "house_finch"], - "13": ["n01534433", "junco"], - "14": ["n01537544", "indigo_bunting"], - "15": ["n01558993", "robin"], - "16": ["n01560419", "bulbul"], - "17": ["n01580077", "jay"], - "18": ["n01582220", "magpie"], - "19": ["n01592084", "chickadee"], - "20": ["n01601694", "water_ouzel"], - "21": ["n01608432", "kite"], - "22": ["n01614925", "bald_eagle"], - "23": ["n01616318", "vulture"], - "24": ["n01622779", "great_grey_owl"], - "25": ["n01629819", "European_fire_salamander"], - "26": ["n01630670", "common_newt"], - "27": ["n01631663", "eft"], - "28": ["n01632458", "spotted_salamander"], - "29": ["n01632777", "axolotl"], - "30": ["n01641577", "bullfrog"], - "31": ["n01644373", "tree_frog"], - "32": ["n01644900", "tailed_frog"], - "33": ["n01664065", "loggerhead"], - "34": ["n01665541", "leatherback_turtle"], - "35": ["n01667114", "mud_turtle"], - "36": ["n01667778", "terrapin"], - "37": ["n01669191", "box_turtle"], - "38": ["n01675722", "banded_gecko"], - "39": ["n01677366", "common_iguana"], - "40": ["n01682714", "American_chameleon"], - "41": ["n01685808", "whiptail"], - "42": ["n01687978", "agama"], - "43": ["n01688243", "frilled_lizard"], - "44": ["n01689811", "alligator_lizard"], - "45": ["n01692333", "Gila_monster"], - "46": ["n01693334", "green_lizard"], - "47": ["n01694178", "African_chameleon"], - "48": ["n01695060", "Komodo_dragon"], - "49": ["n01697457", "African_crocodile"], - "50": ["n01698640", "American_alligator"], - "51": ["n01704323", "triceratops"], - "52": ["n01728572", "thunder_snake"], - "53": ["n01728920", "ringneck_snake"], - "54": ["n01729322", "hognose_snake"], - "55": ["n01729977", "green_snake"], - "56": ["n01734418", "king_snake"], - "57": ["n01735189", "garter_snake"], - "58": ["n01737021", "water_snake"], - "59": ["n01739381", "vine_snake"], - "60": ["n01740131", "night_snake"], - "61": ["n01742172", "boa_constrictor"], - "62": ["n01744401", "rock_python"], - "63": ["n01748264", "Indian_cobra"], - "64": ["n01749939", "green_mamba"], - "65": ["n01751748", "sea_snake"], - "66": ["n01753488", "horned_viper"], - "67": ["n01755581", "diamondback"], - "68": ["n01756291", "sidewinder"], - "69": ["n01768244", "trilobite"], - "70": ["n01770081", "harvestman"], - "71": ["n01770393", "scorpion"], - "72": ["n01773157", "black_and_gold_garden_spider"], - "73": ["n01773549", "barn_spider"], - "74": ["n01773797", "garden_spider"], - "75": ["n01774384", "black_widow"], - "76": ["n01774750", "tarantula"], - "77": ["n01775062", "wolf_spider"], - "78": ["n01776313", "tick"], - "79": ["n01784675", "centipede"], - "80": ["n01795545", "black_grouse"], - "81": ["n01796340", "ptarmigan"], - "82": ["n01797886", "ruffed_grouse"], - "83": ["n01798484", "prairie_chicken"], - "84": ["n01806143", "peacock"], - "85": ["n01806567", "quail"], - "86": ["n01807496", "partridge"], - "87": ["n01817953", "African_grey"], - "88": ["n01818515", "macaw"], - "89": ["n01819313", "sulphur-crested_cockatoo"], - "90": ["n01820546", "lorikeet"], - "91": ["n01824575", "coucal"], - "92": ["n01828970", "bee_eater"], - "93": ["n01829413", "hornbill"], - "94": ["n01833805", "hummingbird"], - "95": ["n01843065", "jacamar"], - "96": ["n01843383", "toucan"], - "97": ["n01847000", "drake"], - "98": ["n01855032", "red-breasted_merganser"], - "99": ["n01855672", "goose"], - "100": ["n01860187", "black_swan"], - "101": ["n01871265", "tusker"], - "102": ["n01872401", "echidna"], - "103": ["n01873310", "platypus"], - "104": ["n01877812", "wallaby"], - "105": ["n01882714", "koala"], - "106": ["n01883070", "wombat"], - "107": ["n01910747", "jellyfish"], - "108": ["n01914609", "sea_anemone"], - "109": ["n01917289", "brain_coral"], - "110": ["n01924916", "flatworm"], - "111": ["n01930112", "nematode"], - "112": ["n01943899", "conch"], - "113": ["n01944390", "snail"], - "114": ["n01945685", "slug"], - "115": ["n01950731", "sea_slug"], - "116": ["n01955084", "chiton"], - "117": ["n01968897", "chambered_nautilus"], - "118": ["n01978287", "Dungeness_crab"], - "119": ["n01978455", "rock_crab"], - "120": ["n01980166", "fiddler_crab"], - "121": ["n01981276", "king_crab"], - "122": ["n01983481", "American_lobster"], - "123": ["n01984695", "spiny_lobster"], - "124": ["n01985128", "crayfish"], - "125": ["n01986214", "hermit_crab"], - "126": ["n01990800", "isopod"], - "127": ["n02002556", "white_stork"], - "128": ["n02002724", "black_stork"], - "129": ["n02006656", "spoonbill"], - "130": ["n02007558", "flamingo"], - "131": ["n02009229", "little_blue_heron"], - "132": ["n02009912", "American_egret"], - "133": ["n02011460", "bittern"], - "134": ["n02012849", "crane"], - "135": ["n02013706", "limpkin"], - "136": ["n02017213", "European_gallinule"], - "137": ["n02018207", "American_coot"], - "138": ["n02018795", "bustard"], - "139": ["n02025239", "ruddy_turnstone"], - "140": ["n02027492", "red-backed_sandpiper"], - "141": ["n02028035", "redshank"], - "142": ["n02033041", "dowitcher"], - "143": ["n02037110", "oystercatcher"], - "144": ["n02051845", "pelican"], - "145": ["n02056570", "king_penguin"], - "146": ["n02058221", "albatross"], - "147": ["n02066245", "grey_whale"], - "148": ["n02071294", "killer_whale"], - "149": ["n02074367", "dugong"], - "150": ["n02077923", "sea_lion"], - "151": ["n02085620", "Chihuahua"], - "152": ["n02085782", "Japanese_spaniel"], - "153": ["n02085936", "Maltese_dog"], - "154": ["n02086079", "Pekinese"], - "155": ["n02086240", "Shih-Tzu"], - "156": ["n02086646", "Blenheim_spaniel"], - "157": ["n02086910", "papillon"], - "158": ["n02087046", "toy_terrier"], - "159": ["n02087394", "Rhodesian_ridgeback"], - "160": ["n02088094", "Afghan_hound"], - "161": ["n02088238", "basset"], - "162": ["n02088364", "beagle"], - "163": ["n02088466", "bloodhound"], - "164": ["n02088632", "bluetick"], - "165": ["n02089078", "black-and-tan_coonhound"], - "166": ["n02089867", "Walker_hound"], - "167": ["n02089973", "English_foxhound"], - "168": ["n02090379", "redbone"], - "169": ["n02090622", "borzoi"], - "170": ["n02090721", "Irish_wolfhound"], - "171": ["n02091032", "Italian_greyhound"], - "172": ["n02091134", "whippet"], - "173": ["n02091244", "Ibizan_hound"], - "174": ["n02091467", "Norwegian_elkhound"], - "175": ["n02091635", "otterhound"], - "176": ["n02091831", "Saluki"], - "177": ["n02092002", "Scottish_deerhound"], - "178": ["n02092339", "Weimaraner"], - "179": ["n02093256", "Staffordshire_bullterrier"], - "180": ["n02093428", "American_Staffordshire_terrier"], - "181": ["n02093647", "Bedlington_terrier"], - "182": ["n02093754", "Border_terrier"], - "183": ["n02093859", "Kerry_blue_terrier"], - "184": ["n02093991", "Irish_terrier"], - "185": ["n02094114", "Norfolk_terrier"], - "186": ["n02094258", "Norwich_terrier"], - "187": ["n02094433", "Yorkshire_terrier"], - "188": ["n02095314", "wire-haired_fox_terrier"], - "189": ["n02095570", "Lakeland_terrier"], - "190": ["n02095889", "Sealyham_terrier"], - "191": ["n02096051", "Airedale"], - "192": ["n02096177", "cairn"], - "193": ["n02096294", "Australian_terrier"], - "194": ["n02096437", "Dandie_Dinmont"], - "195": ["n02096585", "Boston_bull"], - "196": ["n02097047", "miniature_schnauzer"], - "197": ["n02097130", "giant_schnauzer"], - "198": ["n02097209", "standard_schnauzer"], - "199": ["n02097298", "Scotch_terrier"], - "200": ["n02097474", "Tibetan_terrier"], - "201": ["n02097658", "silky_terrier"], - "202": ["n02098105", "soft-coated_wheaten_terrier"], - "203": ["n02098286", "West_Highland_white_terrier"], - "204": ["n02098413", "Lhasa"], - "205": ["n02099267", "flat-coated_retriever"], - "206": ["n02099429", "curly-coated_retriever"], - "207": ["n02099601", "golden_retriever"], - "208": ["n02099712", "Labrador_retriever"], - "209": ["n02099849", "Chesapeake_Bay_retriever"], - "210": ["n02100236", "German_short-haired_pointer"], - "211": ["n02100583", "vizsla"], - "212": ["n02100735", "English_setter"], - "213": ["n02100877", "Irish_setter"], - "214": ["n02101006", "Gordon_setter"], - "215": ["n02101388", "Brittany_spaniel"], - "216": ["n02101556", "clumber"], - "217": ["n02102040", "English_springer"], - "218": ["n02102177", "Welsh_springer_spaniel"], - "219": ["n02102318", "cocker_spaniel"], - "220": ["n02102480", "Sussex_spaniel"], - "221": ["n02102973", "Irish_water_spaniel"], - "222": ["n02104029", "kuvasz"], - "223": ["n02104365", "schipperke"], - "224": ["n02105056", "groenendael"], - "225": ["n02105162", "malinois"], - "226": ["n02105251", "briard"], - "227": ["n02105412", "kelpie"], - "228": ["n02105505", "komondor"], - "229": ["n02105641", "Old_English_sheepdog"], - "230": ["n02105855", "Shetland_sheepdog"], - "231": ["n02106030", "collie"], - "232": ["n02106166", "Border_collie"], - "233": ["n02106382", "Bouvier_des_Flandres"], - "234": ["n02106550", "Rottweiler"], - "235": ["n02106662", "German_shepherd"], - "236": ["n02107142", "Doberman"], - "237": ["n02107312", "miniature_pinscher"], - "238": ["n02107574", "Greater_Swiss_Mountain_dog"], - "239": ["n02107683", "Bernese_mountain_dog"], - "240": ["n02107908", "Appenzeller"], - "241": ["n02108000", "EntleBucher"], - "242": ["n02108089", "boxer"], - "243": ["n02108422", "bull_mastiff"], - "244": ["n02108551", "Tibetan_mastiff"], - "245": ["n02108915", "French_bulldog"], - "246": ["n02109047", "Great_Dane"], - "247": ["n02109525", "Saint_Bernard"], - "248": ["n02109961", "Eskimo_dog"], - "249": ["n02110063", "malamute"], - "250": ["n02110185", "Siberian_husky"], - "251": ["n02110341", "dalmatian"], - "252": ["n02110627", "affenpinscher"], - "253": ["n02110806", "basenji"], - "254": ["n02110958", "pug"], - "255": ["n02111129", "Leonberg"], - "256": ["n02111277", "Newfoundland"], - "257": ["n02111500", "Great_Pyrenees"], - "258": ["n02111889", "Samoyed"], - "259": ["n02112018", "Pomeranian"], - "260": ["n02112137", "chow"], - "261": ["n02112350", "keeshond"], - "262": ["n02112706", "Brabancon_griffon"], - "263": ["n02113023", "Pembroke"], - "264": ["n02113186", "Cardigan"], - "265": ["n02113624", "toy_poodle"], - "266": ["n02113712", "miniature_poodle"], - "267": ["n02113799", "standard_poodle"], - "268": ["n02113978", "Mexican_hairless"], - "269": ["n02114367", "timber_wolf"], - "270": ["n02114548", "white_wolf"], - "271": ["n02114712", "red_wolf"], - "272": ["n02114855", "coyote"], - "273": ["n02115641", "dingo"], - "274": ["n02115913", "dhole"], - "275": ["n02116738", "African_hunting_dog"], - "276": ["n02117135", "hyena"], - "277": ["n02119022", "red_fox"], - "278": ["n02119789", "kit_fox"], - "279": ["n02120079", "Arctic_fox"], - "280": ["n02120505", "grey_fox"], - "281": ["n02123045", "tabby"], - "282": ["n02123159", "tiger_cat"], - "283": ["n02123394", "Persian_cat"], - "284": ["n02123597", "Siamese_cat"], - "285": ["n02124075", "Egyptian_cat"], - "286": ["n02125311", "cougar"], - "287": ["n02127052", "lynx"], - "288": ["n02128385", "leopard"], - "289": ["n02128757", "snow_leopard"], - "290": ["n02128925", "jaguar"], - "291": ["n02129165", "lion"], - "292": ["n02129604", "tiger"], - "293": ["n02130308", "cheetah"], - "294": ["n02132136", "brown_bear"], - "295": ["n02133161", "American_black_bear"], - "296": ["n02134084", "ice_bear"], - "297": ["n02134418", "sloth_bear"], - "298": ["n02137549", "mongoose"], - "299": ["n02138441", "meerkat"], - "300": ["n02165105", "tiger_beetle"], - "301": ["n02165456", "ladybug"], - "302": ["n02167151", "ground_beetle"], - "303": ["n02168699", "long-horned_beetle"], - "304": ["n02169497", "leaf_beetle"], - "305": ["n02172182", "dung_beetle"], - "306": ["n02174001", "rhinoceros_beetle"], - "307": ["n02177972", "weevil"], - "308": ["n02190166", "fly"], - "309": ["n02206856", "bee"], - "310": ["n02219486", "ant"], - "311": ["n02226429", "grasshopper"], - "312": ["n02229544", "cricket"], - "313": ["n02231487", "walking_stick"], - "314": ["n02233338", "cockroach"], - "315": ["n02236044", "mantis"], - "316": ["n02256656", "cicada"], - "317": ["n02259212", "leafhopper"], - "318": ["n02264363", "lacewing"], - "319": ["n02268443", "dragonfly"], - "320": ["n02268853", "damselfly"], - "321": ["n02276258", "admiral"], - "322": ["n02277742", "ringlet"], - "323": ["n02279972", "monarch"], - "324": ["n02280649", "cabbage_butterfly"], - "325": ["n02281406", "sulphur_butterfly"], - "326": ["n02281787", "lycaenid"], - "327": ["n02317335", "starfish"], - "328": ["n02319095", "sea_urchin"], - "329": ["n02321529", "sea_cucumber"], - "330": ["n02325366", "wood_rabbit"], - "331": ["n02326432", "hare"], - "332": ["n02328150", "Angora"], - "333": ["n02342885", "hamster"], - "334": ["n02346627", "porcupine"], - "335": ["n02356798", "fox_squirrel"], - "336": ["n02361337", "marmot"], - "337": ["n02363005", "beaver"], - "338": ["n02364673", "guinea_pig"], - "339": ["n02389026", "sorrel"], - "340": ["n02391049", "zebra"], - "341": ["n02395406", "hog"], - "342": ["n02396427", "wild_boar"], - "343": ["n02397096", "warthog"], - "344": ["n02398521", "hippopotamus"], - "345": ["n02403003", "ox"], - "346": ["n02408429", "water_buffalo"], - "347": ["n02410509", "bison"], - "348": ["n02412080", "ram"], - "349": ["n02415577", "bighorn"], - "350": ["n02417914", "ibex"], - "351": ["n02422106", "hartebeest"], - "352": ["n02422699", "impala"], - "353": ["n02423022", "gazelle"], - "354": ["n02437312", "Arabian_camel"], - "355": ["n02437616", "llama"], - "356": ["n02441942", "weasel"], - "357": ["n02442845", "mink"], - "358": ["n02443114", "polecat"], - "359": ["n02443484", "black-footed_ferret"], - "360": ["n02444819", "otter"], - "361": ["n02445715", "skunk"], - "362": ["n02447366", "badger"], - "363": ["n02454379", "armadillo"], - "364": ["n02457408", "three-toed_sloth"], - "365": ["n02480495", "orangutan"], - "366": ["n02480855", "gorilla"], - "367": ["n02481823", "chimpanzee"], - "368": ["n02483362", "gibbon"], - "369": ["n02483708", "siamang"], - "370": ["n02484975", "guenon"], - "371": ["n02486261", "patas"], - "372": ["n02486410", "baboon"], - "373": ["n02487347", "macaque"], - "374": ["n02488291", "langur"], - "375": ["n02488702", "colobus"], - "376": ["n02489166", "proboscis_monkey"], - "377": ["n02490219", "marmoset"], - "378": ["n02492035", "capuchin"], - "379": ["n02492660", "howler_monkey"], - "380": ["n02493509", "titi"], - "381": ["n02493793", "spider_monkey"], - "382": ["n02494079", "squirrel_monkey"], - "383": ["n02497673", "Madagascar_cat"], - "384": ["n02500267", "indri"], - "385": ["n02504013", "Indian_elephant"], - "386": ["n02504458", "African_elephant"], - "387": ["n02509815", "lesser_panda"], - "388": ["n02510455", "giant_panda"], - "389": ["n02514041", "barracouta"], - "390": ["n02526121", "eel"], - "391": ["n02536864", "coho"], - "392": ["n02606052", "rock_beauty"], - "393": ["n02607072", "anemone_fish"], - "394": ["n02640242", "sturgeon"], - "395": ["n02641379", "gar"], - "396": ["n02643566", "lionfish"], - "397": ["n02655020", "puffer"], - "398": ["n02666196", "abacus"], - "399": ["n02667093", "abaya"], - "400": ["n02669723", "academic_gown"], - "401": ["n02672831", "accordion"], - "402": ["n02676566", "acoustic_guitar"], - "403": ["n02687172", "aircraft_carrier"], - "404": ["n02690373", "airliner"], - "405": ["n02692877", "airship"], - "406": ["n02699494", "altar"], - "407": ["n02701002", "ambulance"], - "408": ["n02704792", "amphibian"], - "409": ["n02708093", "analog_clock"], - "410": ["n02727426", "apiary"], - "411": ["n02730930", "apron"], - "412": ["n02747177", "ashcan"], - "413": ["n02749479", "assault_rifle"], - "414": ["n02769748", "backpack"], - "415": ["n02776631", "bakery"], - "416": ["n02777292", "balance_beam"], - "417": ["n02782093", "balloon"], - "418": ["n02783161", "ballpoint"], - "419": ["n02786058", "Band_Aid"], - "420": ["n02787622", "banjo"], - "421": ["n02788148", "bannister"], - "422": ["n02790996", "barbell"], - "423": ["n02791124", "barber_chair"], - "424": ["n02791270", "barbershop"], - "425": ["n02793495", "barn"], - "426": ["n02794156", "barometer"], - "427": ["n02795169", "barrel"], - "428": ["n02797295", "barrow"], - "429": ["n02799071", "baseball"], - "430": ["n02802426", "basketball"], - "431": ["n02804414", "bassinet"], - "432": ["n02804610", "bassoon"], - "433": ["n02807133", "bathing_cap"], - "434": ["n02808304", "bath_towel"], - "435": ["n02808440", "bathtub"], - "436": ["n02814533", "beach_wagon"], - "437": ["n02814860", "beacon"], - "438": ["n02815834", "beaker"], - "439": ["n02817516", "bearskin"], - "440": ["n02823428", "beer_bottle"], - "441": ["n02823750", "beer_glass"], - "442": ["n02825657", "bell_cote"], - "443": ["n02834397", "bib"], - "444": ["n02835271", "bicycle-built-for-two"], - "445": ["n02837789", "bikini"], - "446": ["n02840245", "binder"], - "447": ["n02841315", "binoculars"], - "448": ["n02843684", "birdhouse"], - "449": ["n02859443", "boathouse"], - "450": ["n02860847", "bobsled"], - "451": ["n02865351", "bolo_tie"], - "452": ["n02869837", "bonnet"], - "453": ["n02870880", "bookcase"], - "454": ["n02871525", "bookshop"], - "455": ["n02877765", "bottlecap"], - "456": ["n02879718", "bow"], - "457": ["n02883205", "bow_tie"], - "458": ["n02892201", "brass"], - "459": ["n02892767", "brassiere"], - "460": ["n02894605", "breakwater"], - "461": ["n02895154", "breastplate"], - "462": ["n02906734", "broom"], - "463": ["n02909870", "bucket"], - "464": ["n02910353", "buckle"], - "465": ["n02916936", "bulletproof_vest"], - "466": ["n02917067", "bullet_train"], - "467": ["n02927161", "butcher_shop"], - "468": ["n02930766", "cab"], - "469": ["n02939185", "caldron"], - "470": ["n02948072", "candle"], - "471": ["n02950826", "cannon"], - "472": ["n02951358", "canoe"], - "473": ["n02951585", "can_opener"], - "474": ["n02963159", "cardigan"], - "475": ["n02965783", "car_mirror"], - "476": ["n02966193", "carousel"], - "477": ["n02966687", "carpenter's_kit"], - "478": ["n02971356", "carton"], - "479": ["n02974003", "car_wheel"], - "480": ["n02977058", "cash_machine"], - "481": ["n02978881", "cassette"], - "482": ["n02979186", "cassette_player"], - "483": ["n02980441", "castle"], - "484": ["n02981792", "catamaran"], - "485": ["n02988304", "CD_player"], - "486": ["n02992211", "cello"], - "487": ["n02992529", "cellular_telephone"], - "488": ["n02999410", "chain"], - "489": ["n03000134", "chainlink_fence"], - "490": ["n03000247", "chain_mail"], - "491": ["n03000684", "chain_saw"], - "492": ["n03014705", "chest"], - "493": ["n03016953", "chiffonier"], - "494": ["n03017168", "chime"], - "495": ["n03018349", "china_cabinet"], - "496": ["n03026506", "Christmas_stocking"], - "497": ["n03028079", "church"], - "498": ["n03032252", "cinema"], - "499": ["n03041632", "cleaver"], - "500": ["n03042490", "cliff_dwelling"], - "501": ["n03045698", "cloak"], - "502": ["n03047690", "clog"], - "503": ["n03062245", "cocktail_shaker"], - "504": ["n03063599", "coffee_mug"], - "505": ["n03063689", "coffeepot"], - "506": ["n03065424", "coil"], - "507": ["n03075370", "combination_lock"], - "508": ["n03085013", "computer_keyboard"], - "509": ["n03089624", "confectionery"], - "510": ["n03095699", "container_ship"], - "511": ["n03100240", "convertible"], - "512": ["n03109150", "corkscrew"], - "513": ["n03110669", "cornet"], - "514": ["n03124043", "cowboy_boot"], - "515": ["n03124170", "cowboy_hat"], - "516": ["n03125729", "cradle"], - "517": ["n03126707", "crane"], - "518": ["n03127747", "crash_helmet"], - "519": ["n03127925", "crate"], - "520": ["n03131574", "crib"], - "521": ["n03133878", "Crock_Pot"], - "522": ["n03134739", "croquet_ball"], - "523": ["n03141823", "crutch"], - "524": ["n03146219", "cuirass"], - "525": ["n03160309", "dam"], - "526": ["n03179701", "desk"], - "527": ["n03180011", "desktop_computer"], - "528": ["n03187595", "dial_telephone"], - "529": ["n03188531", "diaper"], - "530": ["n03196217", "digital_clock"], - "531": ["n03197337", "digital_watch"], - "532": ["n03201208", "dining_table"], - "533": ["n03207743", "dishrag"], - "534": ["n03207941", "dishwasher"], - "535": ["n03208938", "disk_brake"], - "536": ["n03216828", "dock"], - "537": ["n03218198", "dogsled"], - "538": ["n03220513", "dome"], - "539": ["n03223299", "doormat"], - "540": ["n03240683", "drilling_platform"], - "541": ["n03249569", "drum"], - "542": ["n03250847", "drumstick"], - "543": ["n03255030", "dumbbell"], - "544": ["n03259280", "Dutch_oven"], - "545": ["n03271574", "electric_fan"], - "546": ["n03272010", "electric_guitar"], - "547": ["n03272562", "electric_locomotive"], - "548": ["n03290653", "entertainment_center"], - "549": ["n03291819", "envelope"], - "550": ["n03297495", "espresso_maker"], - "551": ["n03314780", "face_powder"], - "552": ["n03325584", "feather_boa"], - "553": ["n03337140", "file"], - "554": ["n03344393", "fireboat"], - "555": ["n03345487", "fire_engine"], - "556": ["n03347037", "fire_screen"], - "557": ["n03355925", "flagpole"], - "558": ["n03372029", "flute"], - "559": ["n03376595", "folding_chair"], - "560": ["n03379051", "football_helmet"], - "561": ["n03384352", "forklift"], - "562": ["n03388043", "fountain"], - "563": ["n03388183", "fountain_pen"], - "564": ["n03388549", "four-poster"], - "565": ["n03393912", "freight_car"], - "566": ["n03394916", "French_horn"], - "567": ["n03400231", "frying_pan"], - "568": ["n03404251", "fur_coat"], - "569": ["n03417042", "garbage_truck"], - "570": ["n03424325", "gasmask"], - "571": ["n03425413", "gas_pump"], - "572": ["n03443371", "goblet"], - "573": ["n03444034", "go-kart"], - "574": ["n03445777", "golf_ball"], - "575": ["n03445924", "golfcart"], - "576": ["n03447447", "gondola"], - "577": ["n03447721", "gong"], - "578": ["n03450230", "gown"], - "579": ["n03452741", "grand_piano"], - "580": ["n03457902", "greenhouse"], - "581": ["n03459775", "grille"], - "582": ["n03461385", "grocery_store"], - "583": ["n03467068", "guillotine"], - "584": ["n03476684", "hair_slide"], - "585": ["n03476991", "hair_spray"], - "586": ["n03478589", "half_track"], - "587": ["n03481172", "hammer"], - "588": ["n03482405", "hamper"], - "589": ["n03483316", "hand_blower"], - "590": ["n03485407", "hand-held_computer"], - "591": ["n03485794", "handkerchief"], - "592": ["n03492542", "hard_disc"], - "593": ["n03494278", "harmonica"], - "594": ["n03495258", "harp"], - "595": ["n03496892", "harvester"], - "596": ["n03498962", "hatchet"], - "597": ["n03527444", "holster"], - "598": ["n03529860", "home_theater"], - "599": ["n03530642", "honeycomb"], - "600": ["n03532672", "hook"], - "601": ["n03534580", "hoopskirt"], - "602": ["n03535780", "horizontal_bar"], - "603": ["n03538406", "horse_cart"], - "604": ["n03544143", "hourglass"], - "605": ["n03584254", "iPod"], - "606": ["n03584829", "iron"], - "607": ["n03590841", "jack-o'-lantern"], - "608": ["n03594734", "jean"], - "609": ["n03594945", "jeep"], - "610": ["n03595614", "jersey"], - "611": ["n03598930", "jigsaw_puzzle"], - "612": ["n03599486", "jinrikisha"], - "613": ["n03602883", "joystick"], - "614": ["n03617480", "kimono"], - "615": ["n03623198", "knee_pad"], - "616": ["n03627232", "knot"], - "617": ["n03630383", "lab_coat"], - "618": ["n03633091", "ladle"], - "619": ["n03637318", "lampshade"], - "620": ["n03642806", "laptop"], - "621": ["n03649909", "lawn_mower"], - "622": ["n03657121", "lens_cap"], - "623": ["n03658185", "letter_opener"], - "624": ["n03661043", "library"], - "625": ["n03662601", "lifeboat"], - "626": ["n03666591", "lighter"], - "627": ["n03670208", "limousine"], - "628": ["n03673027", "liner"], - "629": ["n03676483", "lipstick"], - "630": ["n03680355", "Loafer"], - "631": ["n03690938", "lotion"], - "632": ["n03691459", "loudspeaker"], - "633": ["n03692522", "loupe"], - "634": ["n03697007", "lumbermill"], - "635": ["n03706229", "magnetic_compass"], - "636": ["n03709823", "mailbag"], - "637": ["n03710193", "mailbox"], - "638": ["n03710637", "maillot"], - "639": ["n03710721", "maillot"], - "640": ["n03717622", "manhole_cover"], - "641": ["n03720891", "maraca"], - "642": ["n03721384", "marimba"], - "643": ["n03724870", "mask"], - "644": ["n03729826", "matchstick"], - "645": ["n03733131", "maypole"], - "646": ["n03733281", "maze"], - "647": ["n03733805", "measuring_cup"], - "648": ["n03742115", "medicine_chest"], - "649": ["n03743016", "megalith"], - "650": ["n03759954", "microphone"], - "651": ["n03761084", "microwave"], - "652": ["n03763968", "military_uniform"], - "653": ["n03764736", "milk_can"], - "654": ["n03769881", "minibus"], - "655": ["n03770439", "miniskirt"], - "656": ["n03770679", "minivan"], - "657": ["n03773504", "missile"], - "658": ["n03775071", "mitten"], - "659": ["n03775546", "mixing_bowl"], - "660": ["n03776460", "mobile_home"], - "661": ["n03777568", "Model_T"], - "662": ["n03777754", "modem"], - "663": ["n03781244", "monastery"], - "664": ["n03782006", "monitor"], - "665": ["n03785016", "moped"], - "666": ["n03786901", "mortar"], - "667": ["n03787032", "mortarboard"], - "668": ["n03788195", "mosque"], - "669": ["n03788365", "mosquito_net"], - "670": ["n03791053", "motor_scooter"], - "671": ["n03792782", "mountain_bike"], - "672": ["n03792972", "mountain_tent"], - "673": ["n03793489", "mouse"], - "674": ["n03794056", "mousetrap"], - "675": ["n03796401", "moving_van"], - "676": ["n03803284", "muzzle"], - "677": ["n03804744", "nail"], - "678": ["n03814639", "neck_brace"], - "679": ["n03814906", "necklace"], - "680": ["n03825788", "nipple"], - "681": ["n03832673", "notebook"], - "682": ["n03837869", "obelisk"], - "683": ["n03838899", "oboe"], - "684": ["n03840681", "ocarina"], - "685": ["n03841143", "odometer"], - "686": ["n03843555", "oil_filter"], - "687": ["n03854065", "organ"], - "688": ["n03857828", "oscilloscope"], - "689": ["n03866082", "overskirt"], - "690": ["n03868242", "oxcart"], - "691": ["n03868863", "oxygen_mask"], - "692": ["n03871628", "packet"], - "693": ["n03873416", "paddle"], - "694": ["n03874293", "paddlewheel"], - "695": ["n03874599", "padlock"], - "696": ["n03876231", "paintbrush"], - "697": ["n03877472", "pajama"], - "698": ["n03877845", "palace"], - "699": ["n03884397", "panpipe"], - "700": ["n03887697", "paper_towel"], - "701": ["n03888257", "parachute"], - "702": ["n03888605", "parallel_bars"], - "703": ["n03891251", "park_bench"], - "704": ["n03891332", "parking_meter"], - "705": ["n03895866", "passenger_car"], - "706": ["n03899768", "patio"], - "707": ["n03902125", "pay-phone"], - "708": ["n03903868", "pedestal"], - "709": ["n03908618", "pencil_box"], - "710": ["n03908714", "pencil_sharpener"], - "711": ["n03916031", "perfume"], - "712": ["n03920288", "Petri_dish"], - "713": ["n03924679", "photocopier"], - "714": ["n03929660", "pick"], - "715": ["n03929855", "pickelhaube"], - "716": ["n03930313", "picket_fence"], - "717": ["n03930630", "pickup"], - "718": ["n03933933", "pier"], - "719": ["n03935335", "piggy_bank"], - "720": ["n03937543", "pill_bottle"], - "721": ["n03938244", "pillow"], - "722": ["n03942813", "ping-pong_ball"], - "723": ["n03944341", "pinwheel"], - "724": ["n03947888", "pirate"], - "725": ["n03950228", "pitcher"], - "726": ["n03954731", "plane"], - "727": ["n03956157", "planetarium"], - "728": ["n03958227", "plastic_bag"], - "729": ["n03961711", "plate_rack"], - "730": ["n03967562", "plow"], - "731": ["n03970156", "plunger"], - "732": ["n03976467", "Polaroid_camera"], - "733": ["n03976657", "pole"], - "734": ["n03977966", "police_van"], - "735": ["n03980874", "poncho"], - "736": ["n03982430", "pool_table"], - "737": ["n03983396", "pop_bottle"], - "738": ["n03991062", "pot"], - "739": ["n03992509", "potter's_wheel"], - "740": ["n03995372", "power_drill"], - "741": ["n03998194", "prayer_rug"], - "742": ["n04004767", "printer"], - "743": ["n04005630", "prison"], - "744": ["n04008634", "projectile"], - "745": ["n04009552", "projector"], - "746": ["n04019541", "puck"], - "747": ["n04023962", "punching_bag"], - "748": ["n04026417", "purse"], - "749": ["n04033901", "quill"], - "750": ["n04033995", "quilt"], - "751": ["n04037443", "racer"], - "752": ["n04039381", "racket"], - "753": ["n04040759", "radiator"], - "754": ["n04041544", "radio"], - "755": ["n04044716", "radio_telescope"], - "756": ["n04049303", "rain_barrel"], - "757": ["n04065272", "recreational_vehicle"], - "758": ["n04067472", "reel"], - "759": ["n04069434", "reflex_camera"], - "760": ["n04070727", "refrigerator"], - "761": ["n04074963", "remote_control"], - "762": ["n04081281", "restaurant"], - "763": ["n04086273", "revolver"], - "764": ["n04090263", "rifle"], - "765": ["n04099969", "rocking_chair"], - "766": ["n04111531", "rotisserie"], - "767": ["n04116512", "rubber_eraser"], - "768": ["n04118538", "rugby_ball"], - "769": ["n04118776", "rule"], - "770": ["n04120489", "running_shoe"], - "771": ["n04125021", "safe"], - "772": ["n04127249", "safety_pin"], - "773": ["n04131690", "saltshaker"], - "774": ["n04133789", "sandal"], - "775": ["n04136333", "sarong"], - "776": ["n04141076", "sax"], - "777": ["n04141327", "scabbard"], - "778": ["n04141975", "scale"], - "779": ["n04146614", "school_bus"], - "780": ["n04147183", "schooner"], - "781": ["n04149813", "scoreboard"], - "782": ["n04152593", "screen"], - "783": ["n04153751", "screw"], - "784": ["n04154565", "screwdriver"], - "785": ["n04162706", "seat_belt"], - "786": ["n04179913", "sewing_machine"], - "787": ["n04192698", "shield"], - "788": ["n04200800", "shoe_shop"], - "789": ["n04201297", "shoji"], - "790": ["n04204238", "shopping_basket"], - "791": ["n04204347", "shopping_cart"], - "792": ["n04208210", "shovel"], - "793": ["n04209133", "shower_cap"], - "794": ["n04209239", "shower_curtain"], - "795": ["n04228054", "ski"], - "796": ["n04229816", "ski_mask"], - "797": ["n04235860", "sleeping_bag"], - "798": ["n04238763", "slide_rule"], - "799": ["n04239074", "sliding_door"], - "800": ["n04243546", "slot"], - "801": ["n04251144", "snorkel"], - "802": ["n04252077", "snowmobile"], - "803": ["n04252225", "snowplow"], - "804": ["n04254120", "soap_dispenser"], - "805": ["n04254680", "soccer_ball"], - "806": ["n04254777", "sock"], - "807": ["n04258138", "solar_dish"], - "808": ["n04259630", "sombrero"], - "809": ["n04263257", "soup_bowl"], - "810": ["n04264628", "space_bar"], - "811": ["n04265275", "space_heater"], - "812": ["n04266014", "space_shuttle"], - "813": ["n04270147", "spatula"], - "814": ["n04273569", "speedboat"], - "815": ["n04275548", "spider_web"], - "816": ["n04277352", "spindle"], - "817": ["n04285008", "sports_car"], - "818": ["n04286575", "spotlight"], - "819": ["n04296562", "stage"], - "820": ["n04310018", "steam_locomotive"], - "821": ["n04311004", "steel_arch_bridge"], - "822": ["n04311174", "steel_drum"], - "823": ["n04317175", "stethoscope"], - "824": ["n04325704", "stole"], - "825": ["n04326547", "stone_wall"], - "826": ["n04328186", "stopwatch"], - "827": ["n04330267", "stove"], - "828": ["n04332243", "strainer"], - "829": ["n04335435", "streetcar"], - "830": ["n04336792", "stretcher"], - "831": ["n04344873", "studio_couch"], - "832": ["n04346328", "stupa"], - "833": ["n04347754", "submarine"], - "834": ["n04350905", "suit"], - "835": ["n04355338", "sundial"], - "836": ["n04355933", "sunglass"], - "837": ["n04356056", "sunglasses"], - "838": ["n04357314", "sunscreen"], - "839": ["n04366367", "suspension_bridge"], - "840": ["n04367480", "swab"], - "841": ["n04370456", "sweatshirt"], - "842": ["n04371430", "swimming_trunks"], - "843": ["n04371774", "swing"], - "844": ["n04372370", "switch"], - "845": ["n04376876", "syringe"], - "846": ["n04380533", "table_lamp"], - "847": ["n04389033", "tank"], - "848": ["n04392985", "tape_player"], - "849": ["n04398044", "teapot"], - "850": ["n04399382", "teddy"], - "851": ["n04404412", "television"], - "852": ["n04409515", "tennis_ball"], - "853": ["n04417672", "thatch"], - "854": ["n04418357", "theater_curtain"], - "855": ["n04423845", "thimble"], - "856": ["n04428191", "thresher"], - "857": ["n04429376", "throne"], - "858": ["n04435653", "tile_roof"], - "859": ["n04442312", "toaster"], - "860": ["n04443257", "tobacco_shop"], - "861": ["n04447861", "toilet_seat"], - "862": ["n04456115", "torch"], - "863": ["n04458633", "totem_pole"], - "864": ["n04461696", "tow_truck"], - "865": ["n04462240", "toyshop"], - "866": ["n04465501", "tractor"], - "867": ["n04467665", "trailer_truck"], - "868": ["n04476259", "tray"], - "869": ["n04479046", "trench_coat"], - "870": ["n04482393", "tricycle"], - "871": ["n04483307", "trimaran"], - "872": ["n04485082", "tripod"], - "873": ["n04486054", "triumphal_arch"], - "874": ["n04487081", "trolleybus"], - "875": ["n04487394", "trombone"], - "876": ["n04493381", "tub"], - "877": ["n04501370", "turnstile"], - "878": ["n04505470", "typewriter_keyboard"], - "879": ["n04507155", "umbrella"], - "880": ["n04509417", "unicycle"], - "881": ["n04515003", "upright"], - "882": ["n04517823", "vacuum"], - "883": ["n04522168", "vase"], - "884": ["n04523525", "vault"], - "885": ["n04525038", "velvet"], - "886": ["n04525305", "vending_machine"], - "887": ["n04532106", "vestment"], - "888": ["n04532670", "viaduct"], - "889": ["n04536866", "violin"], - "890": ["n04540053", "volleyball"], - "891": ["n04542943", "waffle_iron"], - "892": ["n04548280", "wall_clock"], - "893": ["n04548362", "wallet"], - "894": ["n04550184", "wardrobe"], - "895": ["n04552348", "warplane"], - "896": ["n04553703", "washbasin"], - "897": ["n04554684", "washer"], - "898": ["n04557648", "water_bottle"], - "899": ["n04560804", "water_jug"], - "900": ["n04562935", "water_tower"], - "901": ["n04579145", "whiskey_jug"], - "902": ["n04579432", "whistle"], - "903": ["n04584207", "wig"], - "904": ["n04589890", "window_screen"], - "905": ["n04590129", "window_shade"], - "906": ["n04591157", "Windsor_tie"], - "907": ["n04591713", "wine_bottle"], - "908": ["n04592741", "wing"], - "909": ["n04596742", "wok"], - "910": ["n04597913", "wooden_spoon"], - "911": ["n04599235", "wool"], - "912": ["n04604644", "worm_fence"], - "913": ["n04606251", "wreck"], - "914": ["n04612504", "yawl"], - "915": ["n04613696", "yurt"], - "916": ["n06359193", "web_site"], - "917": ["n06596364", "comic_book"], - "918": ["n06785654", "crossword_puzzle"], - "919": ["n06794110", "street_sign"], - "920": ["n06874185", "traffic_light"], - "921": ["n07248320", "book_jacket"], - "922": ["n07565083", "menu"], - "923": ["n07579787", "plate"], - "924": ["n07583066", "guacamole"], - "925": ["n07584110", "consomme"], - "926": ["n07590611", "hot_pot"], - "927": ["n07613480", "trifle"], - "928": ["n07614500", "ice_cream"], - "929": ["n07615774", "ice_lolly"], - "930": ["n07684084", "French_loaf"], - "931": ["n07693725", "bagel"], - "932": ["n07695742", "pretzel"], - "933": ["n07697313", "cheeseburger"], - "934": ["n07697537", "hotdog"], - "935": ["n07711569", "mashed_potato"], - "936": ["n07714571", "head_cabbage"], - "937": ["n07714990", "broccoli"], - "938": ["n07715103", "cauliflower"], - "939": ["n07716358", "zucchini"], - "940": ["n07716906", "spaghetti_squash"], - "941": ["n07717410", "acorn_squash"], - "942": ["n07717556", "butternut_squash"], - "943": ["n07718472", "cucumber"], - "944": ["n07718747", "artichoke"], - "945": ["n07720875", "bell_pepper"], - "946": ["n07730033", "cardoon"], - "947": ["n07734744", "mushroom"], - "948": ["n07742313", "Granny_Smith"], - "949": ["n07745940", "strawberry"], - "950": ["n07747607", "orange"], - "951": ["n07749582", "lemon"], - "952": ["n07753113", "fig"], - "953": ["n07753275", "pineapple"], - "954": ["n07753592", "banana"], - "955": ["n07754684", "jackfruit"], - "956": ["n07760859", "custard_apple"], - "957": ["n07768694", "pomegranate"], - "958": ["n07802026", "hay"], - "959": ["n07831146", "carbonara"], - "960": ["n07836838", "chocolate_sauce"], - "961": ["n07860988", "dough"], - "962": ["n07871810", "meat_loaf"], - "963": ["n07873807", "pizza"], - "964": ["n07875152", "potpie"], - "965": ["n07880968", "burrito"], - "966": ["n07892512", "red_wine"], - "967": ["n07920052", "espresso"], - "968": ["n07930864", "cup"], - "969": ["n07932039", "eggnog"], - "970": ["n09193705", "alp"], - "971": ["n09229709", "bubble"], - "972": ["n09246464", "cliff"], - "973": ["n09256479", "coral_reef"], - "974": ["n09288635", "geyser"], - "975": ["n09332890", "lakeside"], - "976": ["n09399592", "promontory"], - "977": ["n09421951", "sandbar"], - "978": ["n09428293", "seashore"], - "979": ["n09468604", "valley"], - "980": ["n09472597", "volcano"], - "981": ["n09835506", "ballplayer"], - "982": ["n10148035", "groom"], - "983": ["n10565667", "scuba_diver"], - "984": ["n11879895", "rapeseed"], - "985": ["n11939491", "daisy"], - "986": ["n12057211", "yellow_lady's_slipper"], - "987": ["n12144580", "corn"], - "988": ["n12267677", "acorn"], - "989": ["n12620546", "hip"], - "990": ["n12768682", "buckeye"], - "991": ["n12985857", "coral_fungus"], - "992": ["n12998815", "agaric"], - "993": ["n13037406", "gyromitra"], - "994": ["n13040303", "stinkhorn"], - "995": ["n13044778", "earthstar"], - "996": ["n13052670", "hen-of-the-woods"], - "997": ["n13054560", "bolete"], - "998": ["n13133613", "ear"], - "999": ["n15075141", "toilet_tissue"] -} diff --git a/machine_learning/app/main.py b/machine_learning/app/main.py deleted file mode 100644 index 966dc8c53dd6e..0000000000000 --- a/machine_learning/app/main.py +++ /dev/null @@ -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", - # ) diff --git a/machine_learning/app/object_detection/__init__.py b/machine_learning/app/object_detection/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/machine_learning/app/object_detection/object_detection.py b/machine_learning/app/object_detection/object_detection.py deleted file mode 100644 index 2f44f7b7482ad..0000000000000 --- a/machine_learning/app/object_detection/object_detection.py +++ /dev/null @@ -1,4 +0,0 @@ - - -def run_detection(): - print("run detection") diff --git a/machine_learning/app/test.png b/machine_learning/app/test.png deleted file mode 100644 index bd77f7719aec5..0000000000000 Binary files a/machine_learning/app/test.png and /dev/null differ diff --git a/machine_learning/requirements.txt b/machine_learning/requirements.txt deleted file mode 100644 index c1d9129272fb6..0000000000000 --- a/machine_learning/requirements.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 6d1832875ec35..9fdb4c1572370 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -23,4 +23,11 @@ + + + + + + + \ No newline at end of file diff --git a/mobile/android/fastlane/metadata/android/en-US/changelogs/17.txt b/mobile/android/fastlane/metadata/android/en-US/changelogs/17.txt new file mode 100644 index 0000000000000..87e139d519991 --- /dev/null +++ b/mobile/android/fastlane/metadata/android/en-US/changelogs/17.txt @@ -0,0 +1 @@ +* Added announcement pop-up when a new released is pushed out in Github. \ No newline at end of file diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 36873521ba505..ffdad9da735f8 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,7 +58,7 @@ UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait @@ -76,5 +76,11 @@ CADisableMinimumFrameDurationOnPhone + + + LSApplicationQueriesSchemes + + https + \ No newline at end of file diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index d6cd6ee4ca8ea..a420a9b67724c 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -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, diff --git a/mobile/lib/constants/hive_box.dart b/mobile/lib/constants/hive_box.dart index 397680dd39c8b..fd9642aa742c5 100644 --- a/mobile/lib/constants/hive_box.dart +++ b/mobile/lib/constants/hive_box.dart @@ -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"; diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7249c712228c0..0cdc0df67f46e 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -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(hiveLoginInfoBox); await Hive.openBox(hiveBackupInfoBox); + await Hive.openBox(hiveGithubReleaseInfoBox); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( @@ -48,10 +52,18 @@ class _ImmichAppState extends ConsumerState 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 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 with WidgetsBindingObserv routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]), ), const ImmichLoadingOverlay(), + const VersionAnnouncementOverlay(), ], ), ); diff --git a/mobile/lib/shared/providers/release_info.provider.dart b/mobile/lib/shared/providers/release_info.provider.dart new file mode 100644 index 0000000000000..dabe2ab7d9a28 --- /dev/null +++ b/mobile/lib/shared/providers/release_info.provider.dart @@ -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 { + 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((ref) => ReleaseInfoNotifier()); diff --git a/mobile/lib/shared/providers/server_info.provider.dart b/mobile/lib/shared/providers/server_info.provider.dart index edba9cdc271bc..36abea4f31293 100644 --- a/mobile/lib/shared/providers/server_info.provider.dart +++ b/mobile/lib/shared/providers/server_info.provider.dart @@ -19,11 +19,6 @@ class ServerInfoNotifier extends StateNotifier { final ServerInfoService _serverInfoService = ServerInfoService(); - getMapboxInfo() async { - MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo(); - state = state.copyWith(mapboxInfo: mapboxInfoRes); - } - getServerVersion() async { ServerVersion? serverVersion = await _serverInfoService.getServerVersion(); diff --git a/mobile/lib/shared/services/server_info.service.dart b/mobile/lib/shared/services/server_info.service.dart index 31d127795b3d7..6fcbda4da7371 100644 --- a/mobile/lib/shared/services/server_info.service.dart +++ b/mobile/lib/shared/services/server_info.service.dart @@ -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 getMapboxInfo() async { - Response response = await _networkService.getRequest(url: 'server-info/mapbox'); - - return MapboxInfo.fromJson(response.toString()); - } - Future 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 ServerVersion.fromJson(response.toString()); + return null; } } diff --git a/mobile/lib/shared/views/version_announcement_overlay.dart b/mobile/lib/shared/views/version_announcement_overlay.dart new file mode 100644 index 0000000000000..474a1340866cf --- /dev/null +++ b/mobile/lib/shared/views/version_announcement_overlay.dart @@ -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( + 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: [ + 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 loaderShowingNotifier = ValueNotifier(false); + ValueNotifier loaderTextNotifier = ValueNotifier('error message'); + + void show() { + loaderShowingNotifier.value = true; + } + + void hide() { + loaderShowingNotifier.value = false; + } +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index f63caf2fc2dc8..a39306df51be9 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -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: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index eec9e7e41dc04..62922e7fe3c6d 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -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: diff --git a/server/Dockerfile b/server/Dockerfile index 902e9aaf31f68..1c1c297969902 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16-alpine3.14 +FROM node:16-alpine3.14 as core ARG DEBIAN_FRONTEND=noninteractive diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts similarity index 84% rename from server/src/api-v1/asset/asset.controller.ts rename to server/apps/immich/src/api-v1/asset/asset.controller.ts index 83316abbfe7d5..75331f6d0a8f8 100644 --- a/server/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -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,13 +66,24 @@ 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.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 }, + ); } - await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size); - this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset)); } catch (e) { Logger.error(`Error receiving upload file ${e}`); diff --git a/server/src/api-v1/asset/asset.module.ts b/server/apps/immich/src/api-v1/asset/asset.module.ts similarity index 59% rename from server/src/api-v1/asset/asset.module.ts rename to server/apps/immich/src/api-v1/asset/asset.module.ts index 20fb5d1613b15..bb9e0823e0e86 100644 --- a/server/src/api-v1/asset/asset.module.ts +++ b/server/apps/immich/src/api-v1/asset/asset.module.ts @@ -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, - }, - }), + BackgroundTaskModule, + TypeOrmModule.forFeature([AssetEntity]), BullModule.registerQueue({ - name: 'background-task', + name: 'asset-uploaded-queue', defaultJobOptions: { attempts: 3, removeOnComplete: true, removeOnFail: false, }, }), - TypeOrmModule.forFeature([AssetEntity]), - ImageOptimizeModule, - BackgroundTaskModule, ], controllers: [AssetController], - providers: [AssetService, AssetOptimizeService, BackgroundTaskService], + providers: [AssetService, BackgroundTaskService], exports: [], }) -export class AssetModule { } +export class AssetModule {} diff --git a/server/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts similarity index 61% rename from server/src/api-v1/asset/asset.service.ts rename to server/apps/immich/src/api-v1/asset/asset.service.ts index 4c598d6acfcfe..3221c64bc566c 100644 --- a/server/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -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, - ) { } - - public async updateThumbnailInfo(assetId: string, path: string) { - return await this.assetRepository.update(assetId, { - resizePath: path, - }); + ) {} + + public async updateThumbnailInfo(asset: AssetEntity, thumbnailPath: string): Promise { + const updatedAsset = await this.assetRepository + .createQueryBuilder('assets') + .update(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); + 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) { - 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 { - res.set({ - 'Content-Type': 'image/jpeg', - }); - file = createReadStream(asset.resizePath); + 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); - - } else if (asset.type == AssetType.VIDEO) { - // Handle Video - let videoPath = asset.originalPath; - let mimeType = asset.mimeType; + file.on('error', (error) => { + Logger.log(`Cannot create read stream ${error}`); + return new BadRequestException('Cannot Create Read Stream'); + }); - if (query.isWeb && asset.mimeType == 'video/quicktime') { - videoPath = asset.encodedVideoPath == '' ? asset.originalPath : asset.encodedVideoPath; - mimeType = asset.encodedVideoPath == '' ? asset.mimeType : 'video/mp4'; + return new StreamableFile(file); + } catch (e) { + Logger.error('Error serving IMAGE asset ', e); + throw new InternalServerErrorException(`Failed to serve image asset ${e}`, 'ServeFile'); } - - 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; + } 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'; } - // 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}`, + 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, }); - 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); - + const videoStream = createReadStream(videoPath, { start: start, end: end }); - } else { - - res.set({ - 'Content-Type': mimeType, - }); + return new StreamableFile(videoStream); + } else { + res.set({ + 'Content-Type': mimeType, + }); - return new StreamableFile(createReadStream(videoPath)); + return new StreamableFile(createReadStream(videoPath)); + } + } catch (e) { + Logger.error('Error serving VIDEO asset ', e); + throw new InternalServerErrorException(`Failed to serve video asset ${e}`, 'ServeFile'); } } } diff --git a/server/src/api-v1/asset/dto/create-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts similarity index 86% rename from server/src/api-v1/asset/dto/create-asset.dto.ts rename to server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts index f3e469ed00e8e..80ebdfd437a77 100644 --- a/server/src/api-v1/asset/dto/create-asset.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts @@ -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() diff --git a/server/src/api-v1/asset/dto/create-exif.dto.ts b/server/apps/immich/src/api-v1/asset/dto/create-exif.dto.ts similarity index 100% rename from server/src/api-v1/asset/dto/create-exif.dto.ts rename to server/apps/immich/src/api-v1/asset/dto/create-exif.dto.ts diff --git a/server/src/api-v1/asset/dto/delete-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/delete-asset.dto.ts similarity index 100% rename from server/src/api-v1/asset/dto/delete-asset.dto.ts rename to server/apps/immich/src/api-v1/asset/dto/delete-asset.dto.ts diff --git a/server/src/api-v1/asset/dto/get-all-asset-query.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-all-asset-query.dto.ts similarity index 100% rename from server/src/api-v1/asset/dto/get-all-asset-query.dto.ts rename to server/apps/immich/src/api-v1/asset/dto/get-all-asset-query.dto.ts diff --git a/server/src/api-v1/asset/dto/get-all-asset-response.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-all-asset-response.dto.ts similarity index 67% rename from server/src/api-v1/asset/dto/get-all-asset-response.dto.ts rename to server/apps/immich/src/api-v1/asset/dto/get-all-asset-response.dto.ts index c26bdff1cffcf..6118786e9edf6 100644 --- a/server/src/api-v1/asset/dto/get-all-asset-response.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/get-all-asset-response.dto.ts @@ -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 }>; diff --git a/server/src/api-v1/asset/dto/get-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts similarity index 100% rename from server/src/api-v1/asset/dto/get-asset.dto.ts rename to server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts diff --git a/server/src/api-v1/asset/dto/get-new-asset-query.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-new-asset-query.dto.ts similarity index 100% rename from server/src/api-v1/asset/dto/get-new-asset-query.dto.ts rename to server/apps/immich/src/api-v1/asset/dto/get-new-asset-query.dto.ts diff --git a/server/src/api-v1/asset/dto/search-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/search-asset.dto.ts similarity index 100% rename from server/src/api-v1/asset/dto/search-asset.dto.ts rename to server/apps/immich/src/api-v1/asset/dto/search-asset.dto.ts diff --git a/server/src/api-v1/asset/dto/serve-file.dto.ts b/server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts similarity index 100% rename from server/src/api-v1/asset/dto/serve-file.dto.ts rename to server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts diff --git a/server/src/api-v1/asset/dto/update-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/update-asset.dto.ts similarity index 100% rename from server/src/api-v1/asset/dto/update-asset.dto.ts rename to server/apps/immich/src/api-v1/asset/dto/update-asset.dto.ts diff --git a/server/src/api-v1/asset/dto/update-exif.dto.ts b/server/apps/immich/src/api-v1/asset/dto/update-exif.dto.ts similarity index 100% rename from server/src/api-v1/asset/dto/update-exif.dto.ts rename to server/apps/immich/src/api-v1/asset/dto/update-exif.dto.ts diff --git a/server/src/api-v1/auth/auth.controller.ts b/server/apps/immich/src/api-v1/auth/auth.controller.ts similarity index 93% rename from server/src/api-v1/auth/auth.controller.ts rename to server/apps/immich/src/api-v1/auth/auth.controller.ts index 27e5d38b05263..16220f2250f4f 100644 --- a/server/src/api-v1/auth/auth.controller.ts +++ b/server/apps/immich/src/api-v1/auth/auth.controller.ts @@ -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) { diff --git a/server/src/api-v1/auth/auth.module.ts b/server/apps/immich/src/api-v1/auth/auth.module.ts similarity index 91% rename from server/src/api-v1/auth/auth.module.ts rename to server/apps/immich/src/api-v1/auth/auth.module.ts index f9b3eee066028..29c009a295c82 100644 --- a/server/src/api-v1/auth/auth.module.ts +++ b/server/apps/immich/src/api-v1/auth/auth.module.ts @@ -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'; diff --git a/server/src/api-v1/auth/auth.service.ts b/server/apps/immich/src/api-v1/auth/auth.service.ts similarity index 98% rename from server/src/api-v1/auth/auth.service.ts rename to server/apps/immich/src/api-v1/auth/auth.service.ts index 8bc2f36231b8b..34a0c478a5a5d 100644 --- a/server/src/api-v1/auth/auth.service.ts +++ b/server/apps/immich/src/api-v1/auth/auth.service.ts @@ -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'; diff --git a/server/src/api-v1/auth/dto/jwt-payload.dto.ts b/server/apps/immich/src/api-v1/auth/dto/jwt-payload.dto.ts similarity index 100% rename from server/src/api-v1/auth/dto/jwt-payload.dto.ts rename to server/apps/immich/src/api-v1/auth/dto/jwt-payload.dto.ts diff --git a/server/src/api-v1/auth/dto/login-credential.dto.ts b/server/apps/immich/src/api-v1/auth/dto/login-credential.dto.ts similarity index 100% rename from server/src/api-v1/auth/dto/login-credential.dto.ts rename to server/apps/immich/src/api-v1/auth/dto/login-credential.dto.ts diff --git a/server/src/api-v1/auth/dto/sign-up.dto.ts b/server/apps/immich/src/api-v1/auth/dto/sign-up.dto.ts similarity index 100% rename from server/src/api-v1/auth/dto/sign-up.dto.ts rename to server/apps/immich/src/api-v1/auth/dto/sign-up.dto.ts diff --git a/server/src/api-v1/communication/communication.gateway.ts b/server/apps/immich/src/api-v1/communication/communication.gateway.ts similarity index 95% rename from server/src/api-v1/communication/communication.gateway.ts rename to server/apps/immich/src/api-v1/communication/communication.gateway.ts index 7bf88411d21df..ae3d4e70319eb 100644 --- a/server/src/api-v1/communication/communication.gateway.ts +++ b/server/apps/immich/src/api-v1/communication/communication.gateway.ts @@ -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() diff --git a/server/src/api-v1/communication/communication.module.ts b/server/apps/immich/src/api-v1/communication/communication.module.ts similarity index 91% rename from server/src/api-v1/communication/communication.module.ts rename to server/apps/immich/src/api-v1/communication/communication.module.ts index d0b0214265e22..95d0360c34e2c 100644 --- a/server/src/api-v1/communication/communication.module.ts +++ b/server/apps/immich/src/api-v1/communication/communication.module.ts @@ -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)], diff --git a/server/src/api-v1/communication/communication.service.ts b/server/apps/immich/src/api-v1/communication/communication.service.ts similarity index 100% rename from server/src/api-v1/communication/communication.service.ts rename to server/apps/immich/src/api-v1/communication/communication.service.ts diff --git a/server/src/api-v1/device-info/device-info.controller.ts b/server/apps/immich/src/api-v1/device-info/device-info.controller.ts similarity index 100% rename from server/src/api-v1/device-info/device-info.controller.ts rename to server/apps/immich/src/api-v1/device-info/device-info.controller.ts diff --git a/server/src/api-v1/device-info/device-info.module.ts b/server/apps/immich/src/api-v1/device-info/device-info.module.ts similarity index 83% rename from server/src/api-v1/device-info/device-info.module.ts rename to server/apps/immich/src/api-v1/device-info/device-info.module.ts index 61a30c3df35e9..59a129f6679fa 100644 --- a/server/src/api-v1/device-info/device-info.module.ts +++ b/server/apps/immich/src/api-v1/device-info/device-info.module.ts @@ -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])], diff --git a/server/src/api-v1/device-info/device-info.service.ts b/server/apps/immich/src/api-v1/device-info/device-info.service.ts similarity index 96% rename from server/src/api-v1/device-info/device-info.service.ts rename to server/apps/immich/src/api-v1/device-info/device-info.service.ts index fa6270780c5d9..718774cbf13f6 100644 --- a/server/src/api-v1/device-info/device-info.service.ts +++ b/server/apps/immich/src/api-v1/device-info/device-info.service.ts @@ -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 { diff --git a/server/src/api-v1/device-info/dto/create-device-info.dto.ts b/server/apps/immich/src/api-v1/device-info/dto/create-device-info.dto.ts similarity index 75% rename from server/src/api-v1/device-info/dto/create-device-info.dto.ts rename to server/apps/immich/src/api-v1/device-info/dto/create-device-info.dto.ts index 9f50e6eaf4143..04c7e0e19059a 100644 --- a/server/src/api-v1/device-info/dto/create-device-info.dto.ts +++ b/server/apps/immich/src/api-v1/device-info/dto/create-device-info.dto.ts @@ -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() diff --git a/server/src/api-v1/device-info/dto/update-device-info.dto.ts b/server/apps/immich/src/api-v1/device-info/dto/update-device-info.dto.ts similarity index 76% rename from server/src/api-v1/device-info/dto/update-device-info.dto.ts rename to server/apps/immich/src/api-v1/device-info/dto/update-device-info.dto.ts index 10efa928c4a9c..f8e954f8e85b6 100644 --- a/server/src/api-v1/device-info/dto/update-device-info.dto.ts +++ b/server/apps/immich/src/api-v1/device-info/dto/update-device-info.dto.ts @@ -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) {} diff --git a/server/src/api-v1/server-info/dto/server-info.dto.ts b/server/apps/immich/src/api-v1/server-info/dto/server-info.dto.ts similarity index 100% rename from server/src/api-v1/server-info/dto/server-info.dto.ts rename to server/apps/immich/src/api-v1/server-info/dto/server-info.dto.ts diff --git a/server/src/api-v1/server-info/server-info.controller.ts b/server/apps/immich/src/api-v1/server-info/server-info.controller.ts similarity index 100% rename from server/src/api-v1/server-info/server-info.controller.ts rename to server/apps/immich/src/api-v1/server-info/server-info.controller.ts diff --git a/server/src/api-v1/server-info/server-info.module.ts b/server/apps/immich/src/api-v1/server-info/server-info.module.ts similarity index 88% rename from server/src/api-v1/server-info/server-info.module.ts rename to server/apps/immich/src/api-v1/server-info/server-info.module.ts index feb31de018187..455b402145933 100644 --- a/server/src/api-v1/server-info/server-info.module.ts +++ b/server/apps/immich/src/api-v1/server-info/server-info.module.ts @@ -4,6 +4,6 @@ import { ServerInfoController } from './server-info.controller'; @Module({ controllers: [ServerInfoController], - providers: [ServerInfoService] + providers: [ServerInfoService], }) export class ServerInfoModule {} diff --git a/server/src/api-v1/server-info/server-info.service.ts b/server/apps/immich/src/api-v1/server-info/server-info.service.ts similarity index 100% rename from server/src/api-v1/server-info/server-info.service.ts rename to server/apps/immich/src/api-v1/server-info/server-info.service.ts diff --git a/server/src/api-v1/sharing/dto/add-assets.dto.ts b/server/apps/immich/src/api-v1/sharing/dto/add-assets.dto.ts similarity index 69% rename from server/src/api-v1/sharing/dto/add-assets.dto.ts rename to server/apps/immich/src/api-v1/sharing/dto/add-assets.dto.ts index a64a602fcf781..22f57c1b3a716 100644 --- a/server/src/api-v1/sharing/dto/add-assets.dto.ts +++ b/server/apps/immich/src/api-v1/sharing/dto/add-assets.dto.ts @@ -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() diff --git a/server/src/api-v1/sharing/dto/add-users.dto.ts b/server/apps/immich/src/api-v1/sharing/dto/add-users.dto.ts similarity index 100% rename from server/src/api-v1/sharing/dto/add-users.dto.ts rename to server/apps/immich/src/api-v1/sharing/dto/add-users.dto.ts diff --git a/server/src/api-v1/sharing/dto/create-shared-album.dto.ts b/server/apps/immich/src/api-v1/sharing/dto/create-shared-album.dto.ts similarity index 76% rename from server/src/api-v1/sharing/dto/create-shared-album.dto.ts rename to server/apps/immich/src/api-v1/sharing/dto/create-shared-album.dto.ts index 5aa59cd38aeb5..6a7194ce3a284 100644 --- a/server/src/api-v1/sharing/dto/create-shared-album.dto.ts +++ b/server/apps/immich/src/api-v1/sharing/dto/create-shared-album.dto.ts @@ -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() diff --git a/server/src/api-v1/sharing/dto/remove-assets.dto.ts b/server/apps/immich/src/api-v1/sharing/dto/remove-assets.dto.ts similarity index 100% rename from server/src/api-v1/sharing/dto/remove-assets.dto.ts rename to server/apps/immich/src/api-v1/sharing/dto/remove-assets.dto.ts diff --git a/server/src/api-v1/sharing/dto/update-shared-album.dto.ts b/server/apps/immich/src/api-v1/sharing/dto/update-shared-album.dto.ts similarity index 100% rename from server/src/api-v1/sharing/dto/update-shared-album.dto.ts rename to server/apps/immich/src/api-v1/sharing/dto/update-shared-album.dto.ts diff --git a/server/src/api-v1/sharing/sharing.controller.ts b/server/apps/immich/src/api-v1/sharing/sharing.controller.ts similarity index 100% rename from server/src/api-v1/sharing/sharing.controller.ts rename to server/apps/immich/src/api-v1/sharing/sharing.controller.ts diff --git a/server/src/api-v1/sharing/sharing.module.ts b/server/apps/immich/src/api-v1/sharing/sharing.module.ts similarity index 55% rename from server/src/api-v1/sharing/sharing.module.ts rename to server/apps/immich/src/api-v1/sharing/sharing.module.ts index 04b511e7d2821..f49e21850fc76 100644 --- a/server/src/api-v1/sharing/sharing.module.ts +++ b/server/apps/immich/src/api-v1/sharing/sharing.module.ts @@ -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: [ diff --git a/server/src/api-v1/sharing/sharing.service.ts b/server/apps/immich/src/api-v1/sharing/sharing.service.ts similarity index 94% rename from server/src/api-v1/sharing/sharing.service.ts rename to server/apps/immich/src/api-v1/sharing/sharing.service.ts index 6a1ffa07e0198..7f8207fcfc84d 100644 --- a/server/src/api-v1/sharing/sharing.service.ts +++ b/server/apps/immich/src/api-v1/sharing/sharing.service.ts @@ -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'; diff --git a/server/src/api-v1/user/dto/create-user.dto.ts b/server/apps/immich/src/api-v1/user/dto/create-user.dto.ts similarity index 100% rename from server/src/api-v1/user/dto/create-user.dto.ts rename to server/apps/immich/src/api-v1/user/dto/create-user.dto.ts diff --git a/server/src/api-v1/user/dto/update-user.dto.ts b/server/apps/immich/src/api-v1/user/dto/update-user.dto.ts similarity index 100% rename from server/src/api-v1/user/dto/update-user.dto.ts rename to server/apps/immich/src/api-v1/user/dto/update-user.dto.ts diff --git a/server/src/api-v1/user/response-dto/user.ts b/server/apps/immich/src/api-v1/user/response-dto/user.ts similarity index 79% rename from server/src/api-v1/user/response-dto/user.ts rename to server/apps/immich/src/api-v1/user/response-dto/user.ts index 9f96380a9ac55..57f503ac162d7 100644 --- a/server/src/api-v1/user/response-dto/user.ts +++ b/server/apps/immich/src/api-v1/user/response-dto/user.ts @@ -1,4 +1,4 @@ -import { UserEntity } from '../entities/user.entity'; +import { UserEntity } from '../../../../../../libs/database/src/entities/user.entity'; export interface User { id: string; diff --git a/server/src/api-v1/user/user.controller.ts b/server/apps/immich/src/api-v1/user/user.controller.ts similarity index 80% rename from server/src/api-v1/user/user.controller.ts rename to server/apps/immich/src/api-v1/user/user.controller.ts index 58c30a9e2ac39..b7da4f87d3b72 100644 --- a/server/src/api-v1/user/user.controller.ts +++ b/server/apps/immich/src/api-v1/user/user.controller.ts @@ -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); } } diff --git a/server/src/api-v1/user/user.module.ts b/server/apps/immich/src/api-v1/user/user.module.ts similarity index 87% rename from server/src/api-v1/user/user.module.ts rename to server/apps/immich/src/api-v1/user/user.module.ts index c348f4bc20c1e..021db43339cd4 100644 --- a/server/src/api-v1/user/user.module.ts +++ b/server/apps/immich/src/api-v1/user/user.module.ts @@ -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 {} diff --git a/server/src/api-v1/user/user.service.ts b/server/apps/immich/src/api-v1/user/user.service.ts similarity index 98% rename from server/src/api-v1/user/user.service.ts rename to server/apps/immich/src/api-v1/user/user.service.ts index e24bb1784eff8..3ce8d38531b73 100644 --- a/server/src/api-v1/user/user.service.ts +++ b/server/apps/immich/src/api-v1/user/user.service.ts @@ -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'; diff --git a/server/src/app.controller.ts b/server/apps/immich/src/app.controller.ts similarity index 80% rename from server/src/app.controller.ts rename to server/apps/immich/src/app.controller.ts index 85fbb096a1f8a..f104e3c77b226 100644 --- a/server/src/app.controller.ts +++ b/server/apps/immich/src/app.controller.ts @@ -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`); } } diff --git a/server/src/app.module.ts b/server/apps/immich/src/app.module.ts similarity index 86% rename from server/src/app.module.ts rename to server/apps/immich/src/app.module.ts index 7aedb33c80eaf..084b01b38ce68 100644 --- a/server/src/app.module.ts +++ b/server/apps/immich/src/app.module.ts @@ -1,6 +1,4 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { databaseConfig } from './config/database.config'; import { UserModule } from './api-v1/user/user.module'; import { AssetModule } from './api-v1/asset/asset.module'; import { AuthModule } from './api-v1/auth/auth.module'; @@ -10,7 +8,6 @@ import { AppLoggerMiddleware } from './middlewares/app-logger.middleware'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { immichAppConfig } from './config/app.config'; import { BullModule } from '@nestjs/bull'; -import { ImageOptimizeModule } from './modules/image-optimize/image-optimize.module'; import { ServerInfoModule } from './api-v1/server-info/server-info.module'; import { BackgroundTaskModule } from './modules/background-task/background-task.module'; import { CommunicationModule } from './api-v1/communication/communication.module'; @@ -18,14 +15,13 @@ import { SharingModule } from './api-v1/sharing/sharing.module'; import { AppController } from './app.controller'; import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module'; - +import { DatabaseModule } from '@app/database'; @Module({ imports: [ ConfigModule.forRoot(immichAppConfig), - TypeOrmModule.forRoot(databaseConfig), - + DatabaseModule, UserModule, AssetModule, @@ -45,8 +41,6 @@ import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.mod }), }), - ImageOptimizeModule, - ServerInfoModule, BackgroundTaskModule, @@ -57,7 +51,7 @@ import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.mod ScheduleModule.forRoot(), - ScheduleTasksModule + ScheduleTasksModule, ], controllers: [AppController], providers: [], diff --git a/server/src/config/app.config.ts b/server/apps/immich/src/config/app.config.ts similarity index 100% rename from server/src/config/app.config.ts rename to server/apps/immich/src/config/app.config.ts diff --git a/server/src/config/asset-upload.config.ts b/server/apps/immich/src/config/asset-upload.config.ts similarity index 99% rename from server/src/config/asset-upload.config.ts rename to server/apps/immich/src/config/asset-upload.config.ts index 4a1c269181933..a868d0c3565ff 100644 --- a/server/src/config/asset-upload.config.ts +++ b/server/apps/immich/src/config/asset-upload.config.ts @@ -56,5 +56,3 @@ export const assetUploadOption: MulterOptions = { }, }), }; - - diff --git a/server/src/config/jwt.config.ts b/server/apps/immich/src/config/jwt.config.ts similarity index 100% rename from server/src/config/jwt.config.ts rename to server/apps/immich/src/config/jwt.config.ts diff --git a/server/src/config/profile-image-upload.config.ts b/server/apps/immich/src/config/profile-image-upload.config.ts similarity index 99% rename from server/src/config/profile-image-upload.config.ts rename to server/apps/immich/src/config/profile-image-upload.config.ts index 29d8be5d5651d..76e65e68c577d 100644 --- a/server/src/config/profile-image-upload.config.ts +++ b/server/apps/immich/src/config/profile-image-upload.config.ts @@ -24,17 +24,13 @@ export const profileImageUploadOption: MulterOptions = { mkdirSync(profileImageLocation, { recursive: true }); } - cb(null, profileImageLocation); }, filename: (req: Request, file: Express.Multer.File, cb: any) => { - const userId = req.user['id']; cb(null, `${userId}${extname(file.originalname)}`); }, }), }; - - diff --git a/server/src/constants/jwt.constant.ts b/server/apps/immich/src/constants/jwt.constant.ts similarity index 100% rename from server/src/constants/jwt.constant.ts rename to server/apps/immich/src/constants/jwt.constant.ts diff --git a/server/src/constants/server_version.constant.ts b/server/apps/immich/src/constants/server_version.constant.ts similarity index 84% rename from server/src/constants/server_version.constant.ts rename to server/apps/immich/src/constants/server_version.constant.ts index 4ae25c1bea91c..97b113bdf299c 100644 --- a/server/src/constants/server_version.constant.ts +++ b/server/apps/immich/src/constants/server_version.constant.ts @@ -3,7 +3,7 @@ export const serverVersion = { major: 1, - minor: 10, + minor: 11, patch: 0, - build: 14, + build: 17, }; diff --git a/server/src/constants/upload_location.constant.ts b/server/apps/immich/src/constants/upload_location.constant.ts similarity index 100% rename from server/src/constants/upload_location.constant.ts rename to server/apps/immich/src/constants/upload_location.constant.ts diff --git a/server/src/decorators/auth-user.decorator.ts b/server/apps/immich/src/decorators/auth-user.decorator.ts similarity index 87% rename from server/src/decorators/auth-user.decorator.ts rename to server/apps/immich/src/decorators/auth-user.decorator.ts index bedd96baed44e..629fa62a8ab35 100644 --- a/server/src/decorators/auth-user.decorator.ts +++ b/server/apps/immich/src/decorators/auth-user.decorator.ts @@ -1,5 +1,5 @@ import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common'; -import { UserEntity } from '../api-v1/user/entities/user.entity'; +import { UserEntity } from '@app/database/entities/user.entity'; // import { AuthUserDto } from './dto/auth-user.dto'; export class AuthUserDto { @@ -18,4 +18,4 @@ export const GetAuthUser = createParamDecorator((data, ctx: ExecutionContext): A }; return authUser; -}); \ No newline at end of file +}); diff --git a/server/src/main.ts b/server/apps/immich/src/main.ts similarity index 94% rename from server/src/main.ts rename to server/apps/immich/src/main.ts index 98487642aa0a4..a61c78caf5832 100644 --- a/server/src/main.ts +++ b/server/apps/immich/src/main.ts @@ -15,11 +15,11 @@ async function bootstrap() { await app.listen(3000, () => { if (process.env.NODE_ENV == 'development') { - Logger.log('Running Immich Server in DEVELOPMENT environment', 'IMMICH SERVER'); + Logger.log('Running Immich Server in DEVELOPMENT environment', 'ImmichServer'); } if (process.env.NODE_ENV == 'production') { - Logger.log('Running Immich Server in PRODUCTION environment', 'IMMICH SERVER'); + Logger.log('Running Immich Server in PRODUCTION environment', 'ImmichServer'); } }); } diff --git a/server/src/middlewares/admin-role-guard.middleware.ts b/server/apps/immich/src/middlewares/admin-role-guard.middleware.ts similarity index 83% rename from server/src/middlewares/admin-role-guard.middleware.ts rename to server/apps/immich/src/middlewares/admin-role-guard.middleware.ts index 84d8f6ac463be..5719328e11b12 100644 --- a/server/src/middlewares/admin-role-guard.middleware.ts +++ b/server/apps/immich/src/middlewares/admin-role-guard.middleware.ts @@ -3,21 +3,23 @@ import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { UserEntity } from '../api-v1/user/entities/user.entity'; +import { UserEntity } from '@app/database/entities/user.entity'; import { ImmichJwtService } from '../modules/immich-jwt/immich-jwt.service'; @Injectable() export class AdminRolesGuard implements CanActivate { - constructor(private reflector: Reflector, private jwtService: ImmichJwtService, + constructor( + private reflector: Reflector, + private jwtService: ImmichJwtService, @InjectRepository(UserEntity) private userRepository: Repository, - ) { } + ) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); if (request.headers['authorization']) { - const bearerToken = request.headers['authorization'].split(" ")[1] + const bearerToken = request.headers['authorization'].split(' ')[1]; const { userId } = await this.jwtService.validateToken(bearerToken); const user = await this.userRepository.findOne(userId); @@ -27,4 +29,4 @@ export class AdminRolesGuard implements CanActivate { return false; } -} \ No newline at end of file +} diff --git a/server/src/middlewares/app-logger.middleware.ts b/server/apps/immich/src/middlewares/app-logger.middleware.ts similarity index 100% rename from server/src/middlewares/app-logger.middleware.ts rename to server/apps/immich/src/middlewares/app-logger.middleware.ts diff --git a/server/src/middlewares/redis-io.adapter.middleware.ts b/server/apps/immich/src/middlewares/redis-io.adapter.middleware.ts similarity index 91% rename from server/src/middlewares/redis-io.adapter.middleware.ts rename to server/apps/immich/src/middlewares/redis-io.adapter.middleware.ts index 45940b04d6ac8..4e06813deddb5 100644 --- a/server/src/middlewares/redis-io.adapter.middleware.ts +++ b/server/apps/immich/src/middlewares/redis-io.adapter.middleware.ts @@ -3,7 +3,7 @@ import { RedisClient } from 'redis'; import { ServerOptions } from 'socket.io'; import { createAdapter } from 'socket.io-redis'; -const redis_host = process.env.REDIS_HOSTNAME || 'immich_redis' +const redis_host = process.env.REDIS_HOSTNAME || 'immich_redis'; // const pubClient = createClient({ url: `redis://${redis_host}:6379` }); // const subClient = pubClient.duplicate(); diff --git a/server/src/modules/background-task/background-task.module.ts b/server/apps/immich/src/modules/background-task/background-task.module.ts similarity index 67% rename from server/src/modules/background-task/background-task.module.ts rename to server/apps/immich/src/modules/background-task/background-task.module.ts index ba912f5b2ff5e..11e1e7a69ec24 100644 --- a/server/src/modules/background-task/background-task.module.ts +++ b/server/apps/immich/src/modules/background-task/background-task.module.ts @@ -1,9 +1,9 @@ import { BullModule } from '@nestjs/bull'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; -import { ExifEntity } from '../../api-v1/asset/entities/exif.entity'; -import { SmartInfoEntity } from '../../api-v1/asset/entities/smart-info.entity'; +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { ExifEntity } from '@app/database/entities/exif.entity'; +import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; import { BackgroundTaskProcessor } from './background-task.processor'; import { BackgroundTaskService } from './background-task.service'; @@ -17,10 +17,9 @@ import { BackgroundTaskService } from './background-task.service'; removeOnFail: false, }, }), - TypeOrmModule.forFeature([AssetEntity, ExifEntity, SmartInfoEntity]), ], providers: [BackgroundTaskService, BackgroundTaskProcessor], - exports: [BackgroundTaskService], + exports: [BackgroundTaskService, BullModule], }) -export class BackgroundTaskModule { } +export class BackgroundTaskModule {} diff --git a/server/apps/immich/src/modules/background-task/background-task.processor.ts b/server/apps/immich/src/modules/background-task/background-task.processor.ts new file mode 100644 index 0000000000000..900b5fffb605f --- /dev/null +++ b/server/apps/immich/src/modules/background-task/background-task.processor.ts @@ -0,0 +1,39 @@ +import { InjectQueue, Process, Processor } from '@nestjs/bull'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Job, Queue } from 'bull'; +import { Repository } from 'typeorm'; +import { AssetEntity } from '@app/database/entities/asset.entity'; +import fs from 'fs'; +import { Logger } from '@nestjs/common'; +import axios from 'axios'; +import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; + +@Processor('background-task') +export class BackgroundTaskProcessor { + constructor( + @InjectRepository(AssetEntity) + private assetRepository: Repository, + + @InjectRepository(SmartInfoEntity) + private smartInfoRepository: Repository, + ) {} + + @Process('delete-file-on-disk') + async deleteFileOnDisk(job) { + const { assets }: { assets: AssetEntity[] } = job.data; + + for (const asset of assets) { + fs.unlink(asset.originalPath, (err) => { + if (err) { + console.log('error deleting ', asset.originalPath); + } + }); + + fs.unlink(asset.resizePath, (err) => { + if (err) { + console.log('error deleting ', asset.originalPath); + } + }); + } + } +} diff --git a/server/apps/immich/src/modules/background-task/background-task.service.ts b/server/apps/immich/src/modules/background-task/background-task.service.ts new file mode 100644 index 0000000000000..6b15b7ff6be4e --- /dev/null +++ b/server/apps/immich/src/modules/background-task/background-task.service.ts @@ -0,0 +1,23 @@ +import { InjectQueue } from '@nestjs/bull/dist/decorators'; +import { Injectable } from '@nestjs/common'; +import { Queue } from 'bull'; +import { randomUUID } from 'node:crypto'; +import { AssetEntity } from '@app/database/entities/asset.entity'; + +@Injectable() +export class BackgroundTaskService { + constructor( + @InjectQueue('background-task') + private backgroundTaskQueue: Queue, + ) {} + + async deleteFileOnDisk(assets: AssetEntity[]) { + await this.backgroundTaskQueue.add( + 'delete-file-on-disk', + { + assets, + }, + { jobId: randomUUID() }, + ); + } +} diff --git a/server/src/modules/immich-jwt/guards/jwt-auth.guard.ts b/server/apps/immich/src/modules/immich-jwt/guards/jwt-auth.guard.ts similarity index 100% rename from server/src/modules/immich-jwt/guards/jwt-auth.guard.ts rename to server/apps/immich/src/modules/immich-jwt/guards/jwt-auth.guard.ts diff --git a/server/src/modules/immich-jwt/immich-jwt.module.ts b/server/apps/immich/src/modules/immich-jwt/immich-jwt.module.ts similarity index 87% rename from server/src/modules/immich-jwt/immich-jwt.module.ts rename to server/apps/immich/src/modules/immich-jwt/immich-jwt.module.ts index 5008613832f13..d7835d277c869 100644 --- a/server/src/modules/immich-jwt/immich-jwt.module.ts +++ b/server/apps/immich/src/modules/immich-jwt/immich-jwt.module.ts @@ -4,7 +4,7 @@ import { JwtModule } from '@nestjs/jwt'; import { jwtConfig } from '../../config/jwt.config'; import { JwtStrategy } from './strategies/jwt.strategy'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserEntity } from '../../api-v1/user/entities/user.entity'; +import { UserEntity } from '@app/database/entities/user.entity'; @Module({ imports: [JwtModule.register(jwtConfig), TypeOrmModule.forFeature([UserEntity])], diff --git a/server/src/modules/immich-jwt/immich-jwt.service.ts b/server/apps/immich/src/modules/immich-jwt/immich-jwt.service.ts similarity index 100% rename from server/src/modules/immich-jwt/immich-jwt.service.ts rename to server/apps/immich/src/modules/immich-jwt/immich-jwt.service.ts diff --git a/server/src/modules/immich-jwt/strategies/jwt.strategy.ts b/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts similarity index 93% rename from server/src/modules/immich-jwt/strategies/jwt.strategy.ts rename to server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts index d4cabe897c516..d83e01ebdb7e7 100644 --- a/server/src/modules/immich-jwt/strategies/jwt.strategy.ts +++ b/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts @@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { Repository } from 'typeorm'; import { JwtPayloadDto } from '../../../api-v1/auth/dto/jwt-payload.dto'; -import { UserEntity } from '../../../api-v1/user/entities/user.entity'; +import { UserEntity } from '@app/database/entities/user.entity'; import { jwtSecret } from '../../../constants/jwt.constant'; @Injectable() diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts new file mode 100644 index 0000000000000..d085cb0398f7e --- /dev/null +++ b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts @@ -0,0 +1,30 @@ +import { BullModule } from '@nestjs/bull'; +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { ScheduleTasksService } from './schedule-tasks.service'; +import { MicroservicesModule } from '../../../../microservices/src/microservices.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([AssetEntity]), + BullModule.registerQueue({ + name: 'video-conversion-queue', + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }), + BullModule.registerQueue({ + name: 'thumbnail-generator-queue', + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }), + ], + providers: [ScheduleTasksService], +}) +export class ScheduleTasksModule {} diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts new file mode 100644 index 0000000000000..aa869480b8c43 --- /dev/null +++ b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts @@ -0,0 +1,60 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; +import { randomUUID } from 'crypto'; + +@Injectable() +export class ScheduleTasksService { + constructor( + @InjectRepository(AssetEntity) + private assetRepository: Repository, + + @InjectQueue('thumbnail-generator-queue') + private thumbnailGeneratorQueue: Queue, + + @InjectQueue('video-conversion-queue') + private videoConversionQueue: Queue, + ) {} + + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) + async webpConversion() { + Logger.log('Starting Schedule Webp Conversion Tasks', 'CronjobWebpGenerator'); + + const assets = await this.assetRepository.find({ + where: { + webpPath: '', + }, + }); + + if (assets.length == 0) { + Logger.log('All assets has webp file - aborting task', 'CronjobWebpGenerator'); + return; + } + + for (const asset of assets) { + await this.thumbnailGeneratorQueue.add('generate-webp-thumbnail', { asset: asset }, { jobId: randomUUID() }); + } + } + + @Cron(CronExpression.EVERY_DAY_AT_1AM) + async videoConversion() { + const assets = await this.assetRepository.find({ + where: { + type: 'VIDEO', + mimeType: 'video/quicktime', + encodedVideoPath: '', + }, + order: { + createdAt: 'DESC', + }, + }); + + for (const asset of assets) { + await this.videoConversionQueue.add('mp4-conversion', { asset }, { jobId: randomUUID() }); + } + } +} diff --git a/server/apps/immich/test/jest-e2e.json b/server/apps/immich/test/jest-e2e.json new file mode 100644 index 0000000000000..7358e01b4f08a --- /dev/null +++ b/server/apps/immich/test/jest-e2e.json @@ -0,0 +1,13 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "moduleNameMapper": { + "@app/database/config/(.*)": "../../../libs/database/src/config/$1", + "@app/database/entities/(.*)": "../../../libs/database/src/entities/$1" + } +} diff --git a/server/test/test-utils.ts b/server/apps/immich/test/test-utils.ts similarity index 100% rename from server/test/test-utils.ts rename to server/apps/immich/test/test-utils.ts diff --git a/server/test/user.e2e-spec.ts b/server/apps/immich/test/user.e2e-spec.ts similarity index 98% rename from server/test/user.e2e-spec.ts rename to server/apps/immich/test/user.e2e-spec.ts index 5657c94efd283..c12ad1d8a3c37 100644 --- a/server/test/user.e2e-spec.ts +++ b/server/apps/immich/test/user.e2e-spec.ts @@ -3,7 +3,7 @@ import { INestApplication } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import request from 'supertest'; import { clearDb, authCustom } from './test-utils'; -import { databaseConfig } from '../src/config/database.config'; +import { databaseConfig } from '@app/database/config/database.config'; import { UserModule } from '../src/api-v1/user/user.module'; import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module'; import { UserService } from '../src/api-v1/user/user.service'; diff --git a/server/apps/immich/tsconfig.app.json b/server/apps/immich/tsconfig.app.json new file mode 100644 index 0000000000000..44a4899dda189 --- /dev/null +++ b/server/apps/immich/tsconfig.app.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/immich" + }, + "include": [ + "src/**/*", + "../../libs/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "test", + "**/*spec.ts" + ] +} \ No newline at end of file diff --git a/server/apps/microservices/src/main.ts b/server/apps/microservices/src/main.ts new file mode 100644 index 0000000000000..efd6fdad7b944 --- /dev/null +++ b/server/apps/microservices/src/main.ts @@ -0,0 +1,18 @@ +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { MicroservicesModule } from './microservices.module'; + +async function bootstrap() { + const app = await NestFactory.create(MicroservicesModule); + + await app.listen(3000, () => { + if (process.env.NODE_ENV == 'development') { + Logger.log('Running Immich Microservices in DEVELOPMENT environment', 'ImmichMicroservice'); + } + + if (process.env.NODE_ENV == 'production') { + Logger.log('Running Immich Microservices in PRODUCTION environment', 'ImmichMicroservice'); + } + }); +} +bootstrap(); diff --git a/server/apps/microservices/src/microservices.module.ts b/server/apps/microservices/src/microservices.module.ts new file mode 100644 index 0000000000000..92c1ce4e75316 --- /dev/null +++ b/server/apps/microservices/src/microservices.module.ts @@ -0,0 +1,70 @@ +import { BullModule } from '@nestjs/bull'; +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DatabaseModule } from '@app/database'; +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { ExifEntity } from '@app/database/entities/exif.entity'; +import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; +import { UserEntity } from '@app/database/entities/user.entity'; +import { MicroservicesService } from './microservices.service'; +import { AssetUploadedProcessor } from './processors/asset-uploaded.processor'; +import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor'; +import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor'; +import { VideoTranscodeProcessor } from './processors/video-transcode.processor'; + +@Module({ + imports: [ + DatabaseModule, + TypeOrmModule.forFeature([UserEntity, ExifEntity, AssetEntity, SmartInfoEntity]), + BullModule.forRootAsync({ + useFactory: async () => ({ + redis: { + host: process.env.REDIS_HOSTNAME || 'immich_redis', + port: 6379, + }, + }), + }), + BullModule.registerQueue({ + name: 'thumbnail-generator-queue', + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }), + BullModule.registerQueue({ + name: 'asset-uploaded-queue', + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }), + BullModule.registerQueue({ + name: 'metadata-extraction-queue', + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }), + BullModule.registerQueue({ + name: 'video-conversion-queue', + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }), + ], + controllers: [], + providers: [ + MicroservicesService, + AssetUploadedProcessor, + ThumbnailGeneratorProcessor, + MetadataExtractionProcessor, + VideoTranscodeProcessor, + ], + exports: [], +}) +export class MicroservicesModule {} diff --git a/server/apps/microservices/src/microservices.service.ts b/server/apps/microservices/src/microservices.service.ts new file mode 100644 index 0000000000000..e2a6ae0c2267e --- /dev/null +++ b/server/apps/microservices/src/microservices.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class MicroservicesService { + getHello(): string { + return 'Hello World 123!'; + } +} diff --git a/server/apps/microservices/src/processors/asset-uploaded.processor.ts b/server/apps/microservices/src/processors/asset-uploaded.processor.ts new file mode 100644 index 0000000000000..c8345c5ca3644 --- /dev/null +++ b/server/apps/microservices/src/processors/asset-uploaded.processor.ts @@ -0,0 +1,67 @@ +import { InjectQueue, OnQueueActive, OnQueueCompleted, OnQueueWaiting, Process, Processor } from '@nestjs/bull'; +import { Job, Queue } from 'bull'; +import { AssetEntity, AssetType } from '@app/database/entities/asset.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { randomUUID } from 'crypto'; + +@Processor('asset-uploaded-queue') +export class AssetUploadedProcessor { + constructor( + @InjectQueue('thumbnail-generator-queue') + private thumbnailGeneratorQueue: Queue, + + @InjectQueue('metadata-extraction-queue') + private metadataExtractionQueue: Queue, + + @InjectQueue('video-conversion-queue') + private videoConversionQueue: Queue, + + @InjectRepository(AssetEntity) + private assetRepository: Repository, + ) {} + + /** + * Post processing uploaded asset to perform the following function if missing + * 1. Generate JPEG Thumbnail + * 2. Generate Webp Thumbnail <-> if JPEG thumbnail exist + * 3. EXIF extractor + * 4. Reverse Geocoding + * + * @param job asset-uploaded + */ + @Process('asset-uploaded') + async processUploadedVideo(job: Job) { + const { + asset, + fileName, + fileSize, + hasThumbnail, + }: { asset: AssetEntity; fileName: string; fileSize: number; hasThumbnail: boolean } = job.data; + + if (hasThumbnail) { + // The jobs below depends on the existence of jpeg thumbnail + await this.thumbnailGeneratorQueue.add('generate-webp-thumbnail', { asset }, { jobId: randomUUID() }); + await this.metadataExtractionQueue.add('tag-image', { asset }, { jobId: randomUUID() }); + await this.metadataExtractionQueue.add('detect-object', { asset }, { jobId: randomUUID() }); + } else { + // Generate Thumbnail -> Then generate webp, tag image and detect object + } + + // Video Conversion + if (asset.type == AssetType.VIDEO) { + await this.videoConversionQueue.add('mp4-conversion', { asset }, { jobId: randomUUID() }); + } else { + // Extract Metadata/Exif for Images - Currently the library cannot extract EXIF for video yet + await this.metadataExtractionQueue.add( + 'exif-extraction', + { + asset, + fileName, + fileSize, + }, + { jobId: randomUUID() }, + ); + } + } +} diff --git a/server/apps/microservices/src/processors/metadata-extraction.processor.ts b/server/apps/microservices/src/processors/metadata-extraction.processor.ts new file mode 100644 index 0000000000000..61da0012afb88 --- /dev/null +++ b/server/apps/microservices/src/processors/metadata-extraction.processor.ts @@ -0,0 +1,131 @@ +import { Process, Processor } from '@nestjs/bull'; +import { Job } from 'bull'; +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { Repository } from 'typeorm/repository/Repository'; +import { InjectRepository } from '@nestjs/typeorm'; +import { ExifEntity } from '@app/database/entities/exif.entity'; +import exifr from 'exifr'; +import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding'; +import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response'; +import { readFile } from 'fs/promises'; +import { Logger } from '@nestjs/common'; +import axios from 'axios'; +import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; + +@Processor('metadata-extraction-queue') +export class MetadataExtractionProcessor { + private geocodingClient: GeocodeService; + + constructor( + @InjectRepository(AssetEntity) + private assetRepository: Repository, + + @InjectRepository(ExifEntity) + private exifRepository: Repository, + + @InjectRepository(SmartInfoEntity) + private smartInfoRepository: Repository, + ) { + if (process.env.ENABLE_MAPBOX) { + this.geocodingClient = mapboxGeocoding({ + accessToken: process.env.MAPBOX_KEY, + }); + } + } + + @Process('exif-extraction') + async extractExifInfo(job: Job) { + try { + const { asset, fileName, fileSize }: { asset: AssetEntity; fileName: string; fileSize: number } = job.data; + + const fileBuffer = await readFile(asset.originalPath); + + const exifData = await exifr.parse(fileBuffer); + + const newExif = new ExifEntity(); + newExif.assetId = asset.id; + newExif.make = exifData['Make'] || null; + newExif.model = exifData['Model'] || null; + newExif.imageName = fileName || null; + newExif.exifImageHeight = exifData['ExifImageHeight'] || null; + newExif.exifImageWidth = exifData['ExifImageWidth'] || null; + newExif.fileSizeInByte = fileSize || null; + newExif.orientation = exifData['Orientation'] || null; + newExif.dateTimeOriginal = exifData['DateTimeOriginal'] || null; + newExif.modifyDate = exifData['ModifyDate'] || null; + newExif.lensModel = exifData['LensModel'] || null; + newExif.fNumber = exifData['FNumber'] || null; + newExif.focalLength = exifData['FocalLength'] || null; + newExif.iso = exifData['ISO'] || null; + newExif.exposureTime = exifData['ExposureTime'] || null; + newExif.latitude = exifData['latitude'] || null; + newExif.longitude = exifData['longitude'] || null; + + // Reverse GeoCoding + if (process.env.ENABLE_MAPBOX && exifData['longitude'] && exifData['latitude']) { + const geoCodeInfo: MapiResponse = await this.geocodingClient + .reverseGeocode({ + query: [exifData['longitude'], exifData['latitude']], + types: ['country', 'region', 'place'], + }) + .send(); + + const res: [] = geoCodeInfo.body['features']; + + const city = res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]['text']; + const state = res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]['text']; + const country = res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]['text']; + + newExif.city = city || null; + newExif.state = state || null; + newExif.country = country || null; + } + + await this.exifRepository.save(newExif); + } catch (e) { + Logger.error(`Error extracting EXIF ${e.toString()}`, 'extractExif'); + } + } + + @Process({ name: 'tag-image', concurrency: 2 }) + async tagImage(job: Job) { + const { asset }: { asset: AssetEntity } = job.data; + + const res = await axios.post('http://immich-machine-learning:3001/image-classifier/tag-image', { + thumbnailPath: asset.resizePath, + }); + + if (res.status == 201 && res.data.length > 0) { + const smartInfo = new SmartInfoEntity(); + smartInfo.assetId = asset.id; + smartInfo.tags = [...res.data]; + + await this.smartInfoRepository.upsert(smartInfo, { + conflictPaths: ['assetId'], + }); + } + } + + @Process({ name: 'detect-object', concurrency: 2 }) + async detectObject(job: Job) { + try { + const { asset }: { asset: AssetEntity } = job.data; + + const res = await axios.post('http://immich-machine-learning:3001/object-detection/detect-object', { + thumbnailPath: asset.resizePath, + }); + + if (res.status == 201 && res.data.length > 0) { + const smartInfo = new SmartInfoEntity(); + smartInfo.assetId = asset.id; + smartInfo.objects = [...res.data]; + + await this.smartInfoRepository.upsert(smartInfo, { + conflictPaths: ['assetId'], + }); + } + } catch (error) { + Logger.error(`Failed to trigger object detection pipe line ${error.toString()}`); + } + } +} diff --git a/server/apps/microservices/src/processors/thumbnail.processor.ts b/server/apps/microservices/src/processors/thumbnail.processor.ts new file mode 100644 index 0000000000000..3f3919daa5180 --- /dev/null +++ b/server/apps/microservices/src/processors/thumbnail.processor.ts @@ -0,0 +1,37 @@ +import { Process, Processor } from '@nestjs/bull'; +import { Job } from 'bull'; +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { Repository } from 'typeorm/repository/Repository'; +import { InjectRepository } from '@nestjs/typeorm'; +import sharp from 'sharp'; + +@Processor('thumbnail-generator-queue') +export class ThumbnailGeneratorProcessor { + constructor( + @InjectRepository(AssetEntity) + private assetRepository: Repository, + ) {} + + @Process('generate-jpeg-thumbnail') + async generateJPEGThumbnail(job: Job) { + const { asset }: { asset: AssetEntity } = job.data; + + console.log(asset); + } + + @Process({ name: 'generate-webp-thumbnail', concurrency: 2 }) + async generateWepbThumbnail(job: Job) { + const { asset }: { asset: AssetEntity } = job.data; + + const webpPath = asset.resizePath.replace('jpeg', 'webp'); + + sharp(asset.resizePath) + .resize(250) + .webp() + .toFile(webpPath, (err, info) => { + if (!err) { + this.assetRepository.update({ id: asset.id }, { webpPath: webpPath }); + } + }); + } +} diff --git a/server/apps/microservices/src/processors/video-transcode.processor.ts b/server/apps/microservices/src/processors/video-transcode.processor.ts new file mode 100644 index 0000000000000..c7f8f3b3baa95 --- /dev/null +++ b/server/apps/microservices/src/processors/video-transcode.processor.ts @@ -0,0 +1,59 @@ +import { Process, Processor } from '@nestjs/bull'; +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Job } from 'bull'; +import ffmpeg from 'fluent-ffmpeg'; +import { existsSync, mkdirSync } from 'fs'; +import { Repository } from 'typeorm'; +import { AssetEntity } from '../../../../libs/database/src/entities/asset.entity'; +import { APP_UPLOAD_LOCATION } from '../../../immich/src/constants/upload_location.constant'; + +@Processor('video-conversion-queue') +export class VideoTranscodeProcessor { + constructor( + @InjectRepository(AssetEntity) + private assetRepository: Repository, + ) {} + + @Process({ name: 'mp4-conversion', concurrency: 1 }) + async mp4Conversion(job: Job) { + const { asset }: { asset: AssetEntity } = job.data; + + if (asset.mimeType != 'video/mp4') { + const basePath = APP_UPLOAD_LOCATION; + const encodedVideoPath = `${basePath}/${asset.userId}/encoded-video`; + + if (!existsSync(encodedVideoPath)) { + mkdirSync(encodedVideoPath, { recursive: true }); + } + + const savedEncodedPath = encodedVideoPath + '/' + asset.id + '.mp4'; + + if (asset.encodedVideoPath == '' || !asset.encodedVideoPath) { + // Put the processing into its own async function to prevent the job exist right away + await this.runFFMPEGPipeLine(asset, savedEncodedPath); + } + } + } + + async runFFMPEGPipeLine(asset: AssetEntity, savedEncodedPath: string): Promise { + return new Promise((resolve, reject) => { + ffmpeg(asset.originalPath) + .outputOptions(['-crf 23', '-preset ultrafast', '-vcodec libx264', '-acodec mp3', '-vf scale=1280:-2']) + .output(savedEncodedPath) + .on('start', () => { + Logger.log('Start Converting', 'mp4Conversion'); + }) + .on('error', (error, b, c) => { + Logger.error(`Cannot Convert Video ${error}`, 'mp4Conversion'); + reject(); + }) + .on('end', async () => { + Logger.log(`Converting Success ${asset.id}`, 'mp4Conversion'); + await this.assetRepository.update({ id: asset.id }, { encodedVideoPath: savedEncodedPath }); + resolve(); + }) + .run(); + }); + } +} diff --git a/microservices/test/app.e2e-spec.ts b/server/apps/microservices/test/app.e2e-spec.ts similarity index 62% rename from microservices/test/app.e2e-spec.ts rename to server/apps/microservices/test/app.e2e-spec.ts index 8a78565051488..edd3fc48cf5f5 100644 --- a/microservices/test/app.e2e-spec.ts +++ b/server/apps/microservices/test/app.e2e-spec.ts @@ -1,15 +1,14 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from '../src/app.module'; +import { MicroservicesModule } from './../src/microservices.module'; -// End to End test -describe('AppController (e2e)', () => { +describe('MicroservicesController (e2e)', () => { let app: INestApplication; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], + imports: [MicroservicesModule], }).compile(); app = moduleFixture.createNestApplication(); @@ -17,9 +16,6 @@ describe('AppController (e2e)', () => { }); it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); + return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); }); }); diff --git a/microservices/test/jest-e2e.json b/server/apps/microservices/test/jest-e2e.json similarity index 100% rename from microservices/test/jest-e2e.json rename to server/apps/microservices/test/jest-e2e.json diff --git a/server/apps/microservices/tsconfig.app.json b/server/apps/microservices/tsconfig.app.json new file mode 100644 index 0000000000000..a24a8136bb8e1 --- /dev/null +++ b/server/apps/microservices/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/microservices" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/server/entrypoint.sh b/server/entrypoint.sh index 7dd3325ba40a9..9f35169bf669e 100644 --- a/server/entrypoint.sh +++ b/server/entrypoint.sh @@ -1,2 +1 @@ -# npm run typeorm migration:run -npm run build && npm run start:prod \ No newline at end of file +npm start immich \ No newline at end of file diff --git a/server/src/config/database.config.ts b/server/libs/database/src/config/database.config.ts similarity index 77% rename from server/src/config/database.config.ts rename to server/libs/database/src/config/database.config.ts index d635b5113a562..618499c2567fb 100644 --- a/server/src/config/database.config.ts +++ b/server/libs/database/src/config/database.config.ts @@ -9,11 +9,12 @@ export const databaseConfig: TypeOrmModuleOptions = { database: process.env.DB_DATABASE_NAME, entities: [__dirname + '/../**/*.entity.{js,ts}'], synchronize: false, - migrations: [__dirname + '/../migration/*.{js,ts}'], + migrations: [__dirname + '/../migrations/*.{js,ts}'], cli: { - migrationsDir: __dirname + '/../migration', + migrationsDir: __dirname + '/../migrations', }, migrationsRun: true, + autoLoadEntities: true, }; export default databaseConfig; diff --git a/server/libs/database/src/database.module.ts b/server/libs/database/src/database.module.ts new file mode 100644 index 0000000000000..df512ee62baa2 --- /dev/null +++ b/server/libs/database/src/database.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { databaseConfig } from './config/database.config'; + +@Module({ + imports: [TypeOrmModule.forRoot(databaseConfig)], + providers: [], + exports: [TypeOrmModule], +}) +export class DatabaseModule {} diff --git a/server/src/api-v1/sharing/entities/asset-shared-album.entity.ts b/server/libs/database/src/entities/asset-shared-album.entity.ts similarity index 92% rename from server/src/api-v1/sharing/entities/asset-shared-album.entity.ts rename to server/libs/database/src/entities/asset-shared-album.entity.ts index 69f4f780426fa..a26c4408110fe 100644 --- a/server/src/api-v1/sharing/entities/asset-shared-album.entity.ts +++ b/server/libs/database/src/entities/asset-shared-album.entity.ts @@ -1,5 +1,5 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; -import { AssetEntity } from '../../asset/entities/asset.entity'; +import { AssetEntity } from './asset.entity'; import { SharedAlbumEntity } from './shared-album.entity'; @Entity('asset_shared_album') diff --git a/server/src/api-v1/asset/entities/asset.entity.ts b/server/libs/database/src/entities/asset.entity.ts similarity index 100% rename from server/src/api-v1/asset/entities/asset.entity.ts rename to server/libs/database/src/entities/asset.entity.ts diff --git a/server/src/api-v1/device-info/entities/device-info.entity.ts b/server/libs/database/src/entities/device-info.entity.ts similarity index 100% rename from server/src/api-v1/device-info/entities/device-info.entity.ts rename to server/libs/database/src/entities/device-info.entity.ts diff --git a/server/src/api-v1/asset/entities/exif.entity.ts b/server/libs/database/src/entities/exif.entity.ts similarity index 100% rename from server/src/api-v1/asset/entities/exif.entity.ts rename to server/libs/database/src/entities/exif.entity.ts diff --git a/server/src/api-v1/sharing/entities/shared-album.entity.ts b/server/libs/database/src/entities/shared-album.entity.ts similarity index 100% rename from server/src/api-v1/sharing/entities/shared-album.entity.ts rename to server/libs/database/src/entities/shared-album.entity.ts diff --git a/server/src/api-v1/asset/entities/smart-info.entity.ts b/server/libs/database/src/entities/smart-info.entity.ts similarity index 100% rename from server/src/api-v1/asset/entities/smart-info.entity.ts rename to server/libs/database/src/entities/smart-info.entity.ts diff --git a/server/src/api-v1/sharing/entities/user-shared-album.entity.ts b/server/libs/database/src/entities/user-shared-album.entity.ts similarity index 91% rename from server/src/api-v1/sharing/entities/user-shared-album.entity.ts rename to server/libs/database/src/entities/user-shared-album.entity.ts index b3041e07c4398..2f4aeb599aca0 100644 --- a/server/src/api-v1/sharing/entities/user-shared-album.entity.ts +++ b/server/libs/database/src/entities/user-shared-album.entity.ts @@ -1,5 +1,5 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; -import { UserEntity } from '../../user/entities/user.entity'; +import { UserEntity } from './user.entity'; import { SharedAlbumEntity } from './shared-album.entity'; @Entity('user_shared_album') diff --git a/server/src/api-v1/user/entities/user.entity.ts b/server/libs/database/src/entities/user.entity.ts similarity index 100% rename from server/src/api-v1/user/entities/user.entity.ts rename to server/libs/database/src/entities/user.entity.ts diff --git a/server/libs/database/src/index.ts b/server/libs/database/src/index.ts new file mode 100644 index 0000000000000..90b0aaab98989 --- /dev/null +++ b/server/libs/database/src/index.ts @@ -0,0 +1 @@ +export * from './database.module'; diff --git a/server/src/migration/1645130759468-CreateUserTable.ts b/server/libs/database/src/migrations/1645130759468-CreateUserTable.ts similarity index 100% rename from server/src/migration/1645130759468-CreateUserTable.ts rename to server/libs/database/src/migrations/1645130759468-CreateUserTable.ts diff --git a/server/src/migration/1645130777674-CreateDeviceInfoTable.ts b/server/libs/database/src/migrations/1645130777674-CreateDeviceInfoTable.ts similarity index 100% rename from server/src/migration/1645130777674-CreateDeviceInfoTable.ts rename to server/libs/database/src/migrations/1645130777674-CreateDeviceInfoTable.ts diff --git a/server/src/migration/1645130805273-CreateAssetsTable.ts b/server/libs/database/src/migrations/1645130805273-CreateAssetsTable.ts similarity index 100% rename from server/src/migration/1645130805273-CreateAssetsTable.ts rename to server/libs/database/src/migrations/1645130805273-CreateAssetsTable.ts diff --git a/server/src/migration/1645130817965-CreateExifTable.ts b/server/libs/database/src/migrations/1645130817965-CreateExifTable.ts similarity index 100% rename from server/src/migration/1645130817965-CreateExifTable.ts rename to server/libs/database/src/migrations/1645130817965-CreateExifTable.ts diff --git a/server/src/migration/1645130870184-CreateSmartInfoTable.ts b/server/libs/database/src/migrations/1645130870184-CreateSmartInfoTable.ts similarity index 100% rename from server/src/migration/1645130870184-CreateSmartInfoTable.ts rename to server/libs/database/src/migrations/1645130870184-CreateSmartInfoTable.ts diff --git a/server/src/migration/1646249209023-AddExifTextSearchColumn.ts b/server/libs/database/src/migrations/1646249209023-AddExifTextSearchColumn.ts similarity index 100% rename from server/src/migration/1646249209023-AddExifTextSearchColumn.ts rename to server/libs/database/src/migrations/1646249209023-AddExifTextSearchColumn.ts diff --git a/server/src/migration/1646249734844-CreateExifTextSearchIndex.ts b/server/libs/database/src/migrations/1646249734844-CreateExifTextSearchIndex.ts similarity index 100% rename from server/src/migration/1646249734844-CreateExifTextSearchIndex.ts rename to server/libs/database/src/migrations/1646249734844-CreateExifTextSearchIndex.ts diff --git a/server/src/migration/1646709533213-AddRegionCityToExIf.ts b/server/libs/database/src/migrations/1646709533213-AddRegionCityToExIf.ts similarity index 100% rename from server/src/migration/1646709533213-AddRegionCityToExIf.ts rename to server/libs/database/src/migrations/1646709533213-AddRegionCityToExIf.ts diff --git a/server/src/migration/1646710459852-AddLocationToExifTextSearch.ts b/server/libs/database/src/migrations/1646710459852-AddLocationToExifTextSearch.ts similarity index 100% rename from server/src/migration/1646710459852-AddLocationToExifTextSearch.ts rename to server/libs/database/src/migrations/1646710459852-AddLocationToExifTextSearch.ts diff --git a/server/src/migration/1648317474768-AddObjectColumnToSmartInfo.ts b/server/libs/database/src/migrations/1648317474768-AddObjectColumnToSmartInfo.ts similarity index 100% rename from server/src/migration/1648317474768-AddObjectColumnToSmartInfo.ts rename to server/libs/database/src/migrations/1648317474768-AddObjectColumnToSmartInfo.ts diff --git a/server/src/migration/1649643216111-CreateSharedAlbumAndRelatedTables.ts b/server/libs/database/src/migrations/1649643216111-CreateSharedAlbumAndRelatedTables.ts similarity index 100% rename from server/src/migration/1649643216111-CreateSharedAlbumAndRelatedTables.ts rename to server/libs/database/src/migrations/1649643216111-CreateSharedAlbumAndRelatedTables.ts diff --git a/server/src/migration/1652633525943-UpdateUserTableWithAdminAndName.ts b/server/libs/database/src/migrations/1652633525943-UpdateUserTableWithAdminAndName.ts similarity index 93% rename from server/src/migration/1652633525943-UpdateUserTableWithAdminAndName.ts rename to server/libs/database/src/migrations/1652633525943-UpdateUserTableWithAdminAndName.ts index 863c4159c7011..af5082ebb2931 100644 --- a/server/src/migration/1652633525943-UpdateUserTableWithAdminAndName.ts +++ b/server/libs/database/src/migrations/1652633525943-UpdateUserTableWithAdminAndName.ts @@ -1,7 +1,6 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class UpdateUserTableWithAdminAndName1652633525943 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` alter table users @@ -18,9 +17,7 @@ export class UpdateUserTableWithAdminAndName1652633525943 implements MigrationIn alter table users add column if not exists "isFirstLoggedIn" bool default true; - `) - - + `); } public async down(queryRunner: QueryRunner): Promise { @@ -36,5 +33,4 @@ export class UpdateUserTableWithAdminAndName1652633525943 implements MigrationIn `); } - } diff --git a/server/src/migration/1653214255670-UpdateAssetTableWithWebpPath.ts b/server/libs/database/src/migrations/1653214255670-UpdateAssetTableWithWebpPath.ts similarity index 86% rename from server/src/migration/1653214255670-UpdateAssetTableWithWebpPath.ts rename to server/libs/database/src/migrations/1653214255670-UpdateAssetTableWithWebpPath.ts index 537c07c337f8b..4de9684f1825c 100644 --- a/server/src/migration/1653214255670-UpdateAssetTableWithWebpPath.ts +++ b/server/libs/database/src/migrations/1653214255670-UpdateAssetTableWithWebpPath.ts @@ -1,12 +1,11 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class UpdateAssetTableWithWebpPath1653214255670 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` alter table assets add column if not exists "webpPath" varchar default ''; - `) + `); } public async down(queryRunner: QueryRunner): Promise { @@ -15,5 +14,4 @@ export class UpdateAssetTableWithWebpPath1653214255670 implements MigrationInter drop column if exists "webpPath"; `); } - } diff --git a/server/src/migration/1654299904583-UpdateAssetTableWithEncodeVideoPath.ts b/server/libs/database/src/migrations/1654299904583-UpdateAssetTableWithEncodeVideoPath.ts similarity index 87% rename from server/src/migration/1654299904583-UpdateAssetTableWithEncodeVideoPath.ts rename to server/libs/database/src/migrations/1654299904583-UpdateAssetTableWithEncodeVideoPath.ts index 0c05b0b9bbdb2..169f7db171da2 100644 --- a/server/src/migration/1654299904583-UpdateAssetTableWithEncodeVideoPath.ts +++ b/server/libs/database/src/migrations/1654299904583-UpdateAssetTableWithEncodeVideoPath.ts @@ -1,11 +1,11 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class UpdateAssetTableWithEncodeVideoPath1654299904583 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` alter table assets add column if not exists "encodedVideoPath" varchar default ''; - `) + `); } public async down(queryRunner: QueryRunner): Promise { diff --git a/server/libs/database/tsconfig.lib.json b/server/libs/database/tsconfig.lib.json new file mode 100644 index 0000000000000..21c8d58b53d1a --- /dev/null +++ b/server/libs/database/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/database" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/server/nest-cli.json b/server/nest-cli.json index 56167b36a14c2..d39f78a0743ff 100644 --- a/server/nest-cli.json +++ b/server/nest-cli.json @@ -1,4 +1,39 @@ { "collection": "@nestjs/schematics", - "sourceRoot": "src" + "sourceRoot": "apps/immich/src", + "monorepo": true, + "root": "apps/immich", + "compilerOptions": { + "webpack": false, + "tsConfigPath": "apps/immich/tsconfig.app.json" + }, + "projects": { + "immich": { + "type": "application", + "root": "apps/immich", + "entryFile": "main", + "sourceRoot": "apps/immich/src", + "compilerOptions": { + "tsConfigPath": "apps/immich/tsconfig.app.json" + } + }, + "microservices": { + "type": "application", + "root": "apps/microservices", + "entryFile": "main", + "sourceRoot": "apps/microservices/src", + "compilerOptions": { + "tsConfigPath": "apps/microservices/tsconfig.app.json" + } + }, + "database": { + "type": "library", + "root": "libs/database", + "entryFile": "index", + "sourceRoot": "libs/database/src", + "compilerOptions": { + "tsConfigPath": "libs/database/tsconfig.lib.json" + } + } + } } diff --git a/server/package.json b/server/package.json index 480e6ea57231d..07edf46f6311b 100644 --- a/server/package.json +++ b/server/package.json @@ -8,7 +8,7 @@ "scripts": { "prebuild": "rimraf dist", "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", @@ -18,8 +18,8 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json", - "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js --config src/config/database.config.ts" + "test:e2e": "jest --config ./apps/immich/test/jest-e2e.json", + "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js --config libs/database/src/config/database.config.ts" }, "dependencies": { "@mapbox/mapbox-sdk": "^0.13.3", @@ -97,7 +97,7 @@ "json", "ts" ], - "rootDir": "src", + "rootDir": ".", "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" @@ -105,7 +105,16 @@ "collectCoverageFrom": [ "**/*.(t|j)s" ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" + "coverageDirectory": "./coverage", + "testEnvironment": "node", + "roots": [ + "/apps/", + "/libs/" + ], + "moduleNameMapper": { + "^@app/database(|/.*)$": "/libs/database/src/$1", + "@app/database/config/(.*)": "/libs/database/src/config/$1", + "@app/database/config": "/libs/database/src/config" + } } } diff --git a/server/settings/nginx-conf/nginx.conf b/server/settings/nginx-conf/nginx.conf deleted file mode 100644 index cb6edc3638403..0000000000000 --- a/server/settings/nginx-conf/nginx.conf +++ /dev/null @@ -1,35 +0,0 @@ - -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} - -# events { -# worker_connections 1000; -# } - -server { - - client_max_body_size 50000M; - - listen 80; - - location / { - proxy_buffering off; - proxy_buffer_size 16k; - proxy_busy_buffers_size 24k; - proxy_buffers 64 4k; - proxy_force_ranges on; - - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - - proxy_pass http://immich_server:3000; - } -} diff --git a/server/src/modules/background-task/background-task.processor.ts b/server/src/modules/background-task/background-task.processor.ts deleted file mode 100644 index 98f7369f70be3..0000000000000 --- a/server/src/modules/background-task/background-task.processor.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { InjectQueue, Process, Processor } from '@nestjs/bull'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Job, Queue } from 'bull'; -import { Repository } from 'typeorm'; -import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; -import { ConfigService } from '@nestjs/config'; -import exifr from 'exifr'; -import { readFile } from 'fs/promises'; -import fs from 'fs'; -import { Logger } from '@nestjs/common'; -import { ExifEntity } from '../../api-v1/asset/entities/exif.entity'; -import axios from 'axios'; -import { SmartInfoEntity } from '../../api-v1/asset/entities/smart-info.entity'; -import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding'; -import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response'; - -@Processor('background-task') -export class BackgroundTaskProcessor { - private geocodingClient: GeocodeService; - - constructor( - @InjectRepository(AssetEntity) - private assetRepository: Repository, - - @InjectRepository(SmartInfoEntity) - private smartInfoRepository: Repository, - - @InjectRepository(ExifEntity) - private exifRepository: Repository, - - private configService: ConfigService, - ) { - if (this.configService.get('ENABLE_MAPBOX')) { - this.geocodingClient = mapboxGeocoding({ - accessToken: this.configService.get('MAPBOX_KEY'), - }); - } - } - - @Process('extract-exif') - async extractExif(job: Job) { - const { savedAsset, fileName, fileSize }: { savedAsset: AssetEntity; fileName: string; fileSize: number } = - job.data; - const fileBuffer = await readFile(savedAsset.originalPath); - - const exifData = await exifr.parse(fileBuffer); - - const newExif = new ExifEntity(); - newExif.assetId = savedAsset.id; - newExif.make = exifData['Make'] || null; - newExif.model = exifData['Model'] || null; - newExif.imageName = fileName || null; - newExif.exifImageHeight = exifData['ExifImageHeight'] || null; - newExif.exifImageWidth = exifData['ExifImageWidth'] || null; - newExif.fileSizeInByte = fileSize || null; - newExif.orientation = exifData['Orientation'] || null; - newExif.dateTimeOriginal = exifData['DateTimeOriginal'] || null; - newExif.modifyDate = exifData['ModifyDate'] || null; - newExif.lensModel = exifData['LensModel'] || null; - newExif.fNumber = exifData['FNumber'] || null; - newExif.focalLength = exifData['FocalLength'] || null; - newExif.iso = exifData['ISO'] || null; - newExif.exposureTime = exifData['ExposureTime'] || null; - newExif.latitude = exifData['latitude'] || null; - newExif.longitude = exifData['longitude'] || null; - - // Reverse GeoCoding - if (this.configService.get('ENABLE_MAPBOX') && exifData['longitude'] && exifData['latitude']) { - const geoCodeInfo: MapiResponse = await this.geocodingClient - .reverseGeocode({ - query: [exifData['longitude'], exifData['latitude']], - types: ['country', 'region', 'place'], - }) - .send(); - - const res: [] = geoCodeInfo.body['features']; - - const city = res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]['text']; - const state = res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]['text']; - const country = res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]['text']; - - newExif.city = city || null; - newExif.state = state || null; - newExif.country = country || null; - } - - await this.exifRepository.save(newExif); - - try { - } catch (e) { - Logger.error(`Error extracting EXIF ${e.toString()}`, 'extractExif'); - } - } - - @Process('delete-file-on-disk') - async deleteFileOnDisk(job) { - const { assets }: { assets: AssetEntity[] } = job.data; - - for (const asset of assets) { - fs.unlink(asset.originalPath, (err) => { - if (err) { - console.log('error deleting ', asset.originalPath); - } - }); - - fs.unlink(asset.resizePath, (err) => { - if (err) { - console.log('error deleting ', asset.originalPath); - } - }); - } - } - - @Process('tag-image') - async tagImage(job) { - const { thumbnailPath, asset }: { thumbnailPath: string; asset: AssetEntity } = job.data; - - const res = await axios.post('http://immich-microservices:3001/image-classifier/tagImage', { - thumbnailPath: thumbnailPath, - }); - - if (res.status == 201 && res.data.length > 0) { - const smartInfo = new SmartInfoEntity(); - smartInfo.assetId = asset.id; - smartInfo.tags = [...res.data]; - - await this.smartInfoRepository.upsert(smartInfo, { - conflictPaths: ['assetId'], - }); - } - } - - @Process('detect-object') - async detectObject(job) { - try { - const { thumbnailPath, asset }: { thumbnailPath: string; asset: AssetEntity } = job.data; - - const res = await axios.post('http://immich-microservices:3001/object-detection/detectObject', { - thumbnailPath: thumbnailPath, - }); - - if (res.status == 201 && res.data.length > 0) { - const smartInfo = new SmartInfoEntity(); - smartInfo.assetId = asset.id; - smartInfo.objects = [...res.data]; - await this.smartInfoRepository.upsert(smartInfo, { - conflictPaths: ['assetId'], - }); - } - } catch (error) { - Logger.error(`Failed to trigger object detection pipe line ${error.toString()}`) - } - - } -} diff --git a/server/src/modules/background-task/background-task.service.ts b/server/src/modules/background-task/background-task.service.ts deleted file mode 100644 index 1a62ce54d5ee1..0000000000000 --- a/server/src/modules/background-task/background-task.service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { InjectQueue } from '@nestjs/bull/dist/decorators'; -import { Injectable } from '@nestjs/common'; -import { Queue } from 'bull'; -import { randomUUID } from 'node:crypto'; -import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; - -@Injectable() -export class BackgroundTaskService { - constructor( - @InjectQueue('background-task') - private backgroundTaskQueue: Queue, - ) { } - - async extractExif(savedAsset: AssetEntity, fileName: string, fileSize: number) { - await this.backgroundTaskQueue.add( - 'extract-exif', - { - savedAsset, - fileName, - fileSize, - }, - { jobId: randomUUID() }, - ); - } - - async deleteFileOnDisk(assets: AssetEntity[]) { - await this.backgroundTaskQueue.add( - 'delete-file-on-disk', - { - assets, - }, - { jobId: randomUUID() }, - ); - } - - async tagImage(thumbnailPath: string, asset: AssetEntity) { - await this.backgroundTaskQueue.add( - 'tag-image', - { - thumbnailPath, - asset, - }, - { jobId: randomUUID() }, - ); - } - - async detectObject(thumbnailPath: string, asset: AssetEntity) { - await this.backgroundTaskQueue.add( - 'detect-object', - { - thumbnailPath, - asset, - }, - { jobId: randomUUID() }, - ); - } -} diff --git a/server/src/modules/image-optimize/image-optimize.module.ts b/server/src/modules/image-optimize/image-optimize.module.ts deleted file mode 100644 index 3d58ad8d0ca77..0000000000000 --- a/server/src/modules/image-optimize/image-optimize.module.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { BullModule } from '@nestjs/bull'; -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; -import { CommunicationModule } from '../../api-v1/communication/communication.module'; -import { BackgroundTaskModule } from '../background-task/background-task.module'; -import { BackgroundTaskService } from '../background-task/background-task.service'; -import { ImageOptimizeProcessor } from './image-optimize.processor'; -import { AssetOptimizeService } from './image-optimize.service'; - -@Module({ - imports: [ - CommunicationModule, - BackgroundTaskModule, - 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]), - ], - providers: [AssetOptimizeService, ImageOptimizeProcessor, BackgroundTaskService], - exports: [AssetOptimizeService], -}) -export class ImageOptimizeModule { } diff --git a/server/src/modules/image-optimize/image-optimize.processor.ts b/server/src/modules/image-optimize/image-optimize.processor.ts deleted file mode 100644 index f5bcecc7eff4d..0000000000000 --- a/server/src/modules/image-optimize/image-optimize.processor.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Processor } from '@nestjs/bull'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; -import { CommunicationGateway } from '../../api-v1/communication/communication.gateway'; -import { BackgroundTaskService } from '../background-task/background-task.service'; - -@Processor('optimize') -export class ImageOptimizeProcessor { - constructor( - private wsCommunicateionGateway: CommunicationGateway, - @InjectRepository(AssetEntity) - private assetRepository: Repository, - - private backgroundTaskService: BackgroundTaskService, - ) {} -} diff --git a/server/src/modules/image-optimize/image-optimize.service.ts b/server/src/modules/image-optimize/image-optimize.service.ts deleted file mode 100644 index ae8b711e3d14d..0000000000000 --- a/server/src/modules/image-optimize/image-optimize.service.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { InjectQueue } from '@nestjs/bull'; -import { Injectable } from '@nestjs/common'; -import { Queue } from 'bull'; -import { randomUUID } from 'crypto'; -import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; - -@Injectable() -export class AssetOptimizeService { - constructor(@InjectQueue('optimize') private optimizeQueue: Queue) {} -} diff --git a/server/src/modules/schedule-tasks/image-conversion.service.ts b/server/src/modules/schedule-tasks/image-conversion.service.ts deleted file mode 100644 index b9690ca8fba48..0000000000000 --- a/server/src/modules/schedule-tasks/image-conversion.service.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Cron, CronExpression } from '@nestjs/schedule'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; -import sharp from 'sharp'; - -@Injectable() -export class ImageConversionService { - - constructor( - @InjectRepository(AssetEntity) - private assetRepository: Repository - ) { } - - @Cron(CronExpression.EVERY_5_MINUTES - , { - name: 'webp-conversion' - }) - async webpConversion() { - Logger.log('Starting Webp Conversion Tasks', 'ImageConversionService') - - const assets = await this.assetRepository.find({ - where: { - webpPath: '' - }, - take: 500 - }); - - - if (assets.length == 0) { - Logger.log('All assets has webp file - aborting task', 'ImageConversionService') - return; - } - - - for (const asset of assets) { - const resizePath = asset.resizePath; - if (resizePath != '') { - const webpPath = resizePath.replace('jpeg', 'webp') - - sharp(resizePath).resize(250).webp().toFile(webpPath, (err, info) => { - - if (!err) { - this.assetRepository.update({ id: asset.id }, { webpPath: webpPath }) - } - - }); - } - } - } -} diff --git a/server/src/modules/schedule-tasks/schedule-tasks.module.ts b/server/src/modules/schedule-tasks/schedule-tasks.module.ts deleted file mode 100644 index a37054df94005..0000000000000 --- a/server/src/modules/schedule-tasks/schedule-tasks.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { BullModule } from '@nestjs/bull'; -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { AssetModule } from '../../api-v1/asset/asset.module'; -import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; -import { ImageConversionService } from './image-conversion.service'; -import { VideoConversionProcessor } from './video-conversion.processor'; -import { VideoConversionService } from './video-conversion.service'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([AssetEntity]), - - BullModule.registerQueue({ - settings: {}, - name: 'video-conversion', - limiter: { - max: 1, - duration: 60000 - }, - defaultJobOptions: { - attempts: 3, - removeOnComplete: true, - removeOnFail: false, - }, - }), - ], - providers: [ImageConversionService, VideoConversionService, VideoConversionProcessor,], -}) -export class ScheduleTasksModule { } diff --git a/server/src/modules/schedule-tasks/video-conversion.processor.ts b/server/src/modules/schedule-tasks/video-conversion.processor.ts deleted file mode 100644 index da64d08acc360..0000000000000 --- a/server/src/modules/schedule-tasks/video-conversion.processor.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Process, Processor } from '@nestjs/bull'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Job } from 'bull'; -import { Repository } from 'typeorm'; -import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; -import { existsSync, mkdirSync } from 'fs'; -import { APP_UPLOAD_LOCATION } from '../../constants/upload_location.constant'; -import ffmpeg from 'fluent-ffmpeg'; -import { Logger } from '@nestjs/common'; - -@Processor('video-conversion') -export class VideoConversionProcessor { - - constructor( - @InjectRepository(AssetEntity) - private assetRepository: Repository, - ) { } - - @Process('to-mp4') - async convertToMp4(job: Job) { - const { asset }: { asset: AssetEntity } = job.data; - - const basePath = APP_UPLOAD_LOCATION; - const encodedVideoPath = `${basePath}/${asset.userId}/encoded-video`; - - if (!existsSync(encodedVideoPath)) { - mkdirSync(encodedVideoPath, { recursive: true }); - } - - const latestAssetInfo = await this.assetRepository.findOne({ id: asset.id }); - const savedEncodedPath = encodedVideoPath + "/" + latestAssetInfo.id + '.mp4' - - if (latestAssetInfo.encodedVideoPath == '') { - ffmpeg(latestAssetInfo.originalPath) - .outputOptions([ - '-crf 23', - '-preset ultrafast', - '-vcodec libx264', - '-acodec mp3', - '-vf scale=1280:-2' - ]) - .output(savedEncodedPath) - .on('start', () => Logger.log("Start Converting", 'VideoConversionMOV2MP4')) - .on('error', (a, b, c) => { - Logger.error('Cannot Convert Video', 'VideoConversionMOV2MP4') - console.log(a, b, c) - }) - .on('end', async () => { - Logger.log(`Converting Success ${latestAssetInfo.id}`, 'VideoConversionMOV2MP4') - await this.assetRepository.update({ id: latestAssetInfo.id }, { encodedVideoPath: savedEncodedPath }); - }).run(); - } - - return {} - } -} diff --git a/server/src/modules/schedule-tasks/video-conversion.service.ts b/server/src/modules/schedule-tasks/video-conversion.service.ts deleted file mode 100644 index d2a1a82a433b6..0000000000000 --- a/server/src/modules/schedule-tasks/video-conversion.service.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Cron, CronExpression } from '@nestjs/schedule'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; -import sharp from 'sharp'; -import ffmpeg from 'fluent-ffmpeg'; -import { APP_UPLOAD_LOCATION } from '../../constants/upload_location.constant'; -import { existsSync, mkdirSync } from 'fs'; -import { InjectQueue } from '@nestjs/bull/dist/decorators'; -import { Queue } from 'bull'; -import { randomUUID } from 'crypto'; - -@Injectable() -export class VideoConversionService { - - - constructor( - @InjectRepository(AssetEntity) - private assetRepository: Repository, - - @InjectQueue('video-conversion') - private videoEncodingQueue: Queue - ) { } - - - @Cron(CronExpression.EVERY_MINUTE - , { - name: 'video-encoding' - }) - async mp4Conversion() { - const assets = await this.assetRepository.find({ - where: { - type: 'VIDEO', - mimeType: 'video/quicktime', - encodedVideoPath: '' - }, - order: { - createdAt: 'DESC' - }, - take: 1 - }); - - if (assets.length > 0) { - const asset = assets[0]; - await this.videoEncodingQueue.add('to-mp4', { asset }, { jobId: asset.id },) - } - } -} diff --git a/server/start-microservices.sh b/server/start-microservices.sh new file mode 100644 index 0000000000000..4e1f9c12ebddd --- /dev/null +++ b/server/start-microservices.sh @@ -0,0 +1 @@ +npm start microservices \ No newline at end of file diff --git a/server/start-server.sh b/server/start-server.sh new file mode 100644 index 0000000000000..9f35169bf669e --- /dev/null +++ b/server/start-server.sh @@ -0,0 +1 @@ +npm start immich \ No newline at end of file diff --git a/server/test/jest-e2e.json b/server/test/jest-e2e.json deleted file mode 100644 index e9d912f3e3cef..0000000000000 --- a/server/test/jest-e2e.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - } -} diff --git a/server/tsconfig.json b/server/tsconfig.json index 7039c7253a231..e7e31bffa63fa 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -6,16 +6,26 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, "target": "es2017", "sourceMap": true, "outDir": "./dist", "incremental": true, "skipLibCheck": true, "esModuleInterop": true, + "baseUrl": "./", + "paths": { + "@app/database": [ + "libs/database/src" + ], + "@app/database/*": [ + "libs/database/src/*" + ] + } }, "exclude": [ "dist", "node_modules", "upload" - ], + ] } \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index dfee43d84b0f5..89a4d7d3c289d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,6 +13,7 @@ "leaflet": "^1.8.0", "lodash": "^4.17.21", "lodash-es": "^4.17.21", + "markdown-it": "^13.0.1", "moment": "^2.29.3", "svelte-material-icons": "^2.0.2" }, @@ -27,6 +28,7 @@ "@types/leaflet": "^1.7.10", "@types/lodash": "^4.14.182", "@types/lodash-es": "^4.17.6", + "@types/markdown-it": "^12.2.3", "@typescript-eslint/eslint-plugin": "^5.10.1", "@typescript-eslint/parser": "^5.10.1", "autoprefixer": "^10.4.7", @@ -291,6 +293,12 @@ "@types/geojson": "*" } }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "dev": true + }, "node_modules/@types/lodash": { "version": "4.14.182", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", @@ -306,6 +314,22 @@ "@types/lodash": "*" } }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "dev": true + }, "node_modules/@types/node": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz", @@ -626,8 +650,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-union": { "version": "2.1.0", @@ -1020,6 +1043,17 @@ "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", "dev": true }, + "node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -2078,6 +2112,14 @@ "node": ">=10" } }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2118,6 +2160,26 @@ "node": ">=12" } }, + "node_modules/markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3125,6 +3187,11 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3458,6 +3525,12 @@ "@types/geojson": "*" } }, + "@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "dev": true + }, "@types/lodash": { "version": "4.14.182", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", @@ -3473,6 +3546,22 @@ "@types/lodash": "*" } }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "dev": true + }, "@types/node": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz", @@ -3673,8 +3762,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "array-union": { "version": "2.1.0", @@ -3940,6 +4028,11 @@ "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", "dev": true }, + "entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==" + }, "es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -4636,6 +4729,14 @@ "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", "dev": true }, + "linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "requires": { + "uc.micro": "^1.0.1" + } + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -4670,6 +4771,23 @@ "sourcemap-codec": "^1.4.8" } }, + "markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "requires": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5318,6 +5436,11 @@ "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", "dev": true }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/web/src/hooks.ts b/web/src/hooks.ts index b9efecb3ae745..400bf275fe68f 100644 --- a/web/src/hooks.ts +++ b/web/src/hooks.ts @@ -3,58 +3,54 @@ import * as cookie from 'cookie'; import { serverEndpoint } from '$lib/constants'; import { session } from '$app/stores'; - -export const handle: Handle = async ({ event, resolve, }) => { - const cookies = cookie.parse(event.request.headers.get('cookie') || ''); - - if (!cookies.session) { - return await resolve(event) - } - - try { - const { email, isAdmin, firstName, lastName, id, accessToken } = JSON.parse(cookies.session); - - const res = await fetch(`${serverEndpoint}/auth/validateToken`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${accessToken}` - } - }) - - if (res.status === 201) { - event.locals.user = { - id, - accessToken, - firstName, - lastName, - isAdmin, - email - }; - } - - const response = await resolve(event); - - return response; - } catch (error) { - console.log('Error parsing session', error); - return await resolve(event); - } - +export const handle: Handle = async ({ event, resolve }) => { + const cookies = cookie.parse(event.request.headers.get('cookie') || ''); + + if (!cookies.session) { + return await resolve(event); + } + + try { + const { email, isAdmin, firstName, lastName, id, accessToken } = JSON.parse(cookies.session); + + const res = await fetch(`${serverEndpoint}/auth/validateToken`, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (res.status === 201) { + event.locals.user = { + id, + accessToken, + firstName, + lastName, + isAdmin, + email, + }; + } + + const response = await resolve(event); + + return response; + } catch (error) { + console.log('Error parsing session', error); + return await resolve(event); + } }; export const getSession: GetSession = async ({ locals }) => { - - if (!locals.user) return {} - - return { - user: { - id: locals.user.id, - accessToken: locals.user.accessToken, - firstName: locals.user.firstName, - lastName: locals.user.lastName, - isAdmin: locals.user.isAdmin, - email: locals.user.email - } - } -} - + if (!locals.user) return {}; + + return { + user: { + id: locals.user.id, + accessToken: locals.user.accessToken, + firstName: locals.user.firstName, + lastName: locals.user.lastName, + isAdmin: locals.user.isAdmin, + email: locals.user.email, + }, + }; +}; diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index b5e7bc772ed9b..b624a204fefe2 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -1,60 +1,59 @@ import { serverEndpoint } from './constants'; type ISend = { - method: string, - path: string, - data?: any, - token: string - customHeaders?: Record, -} + method: string; + path: string; + data?: any; + token: string; + customHeaders?: Record; +}; type IOption = { - method: string, - headers: Record, - body: any - -} + method: string; + headers: Record; + body: any; +}; async function send({ method, path, data, token, customHeaders }: ISend) { - const opts: IOption = { method, headers: {} } as IOption; - - if (data) { - opts.headers['Content-Type'] = 'application/json'; - opts.body = JSON.stringify(data); - } - - if (customHeaders) { - console.log(customHeaders); - // opts.headers[customHeader.$1] - } - - if (token) { - opts.headers['Authorization'] = `Bearer ${token}`; - } - - return fetch(`${serverEndpoint}/${path}`, opts) - .then((r) => r.text()) - .then((json) => { - try { - return JSON.parse(json); - } catch (err) { - return json; - } - }); + const opts: IOption = { method, headers: {} } as IOption; + + if (data) { + opts.headers['Content-Type'] = 'application/json'; + opts.body = JSON.stringify(data); + } + + if (customHeaders) { + console.log(customHeaders); + // opts.headers[customHeader.$1] + } + + if (token) { + opts.headers['Authorization'] = `Bearer ${token}`; + } + + return fetch(`${serverEndpoint}/${path}`, opts) + .then((r) => r.text()) + .then((json) => { + try { + return JSON.parse(json); + } catch (err) { + return json; + } + }); } export function getRequest(path: string, token: string, customHeaders?: Record) { - return send({ method: 'GET', path, token, customHeaders }); + return send({ method: 'GET', path, token, customHeaders }); } export function delRequest(path: string, token: string, customHeaders?: Record) { - return send({ method: 'DELETE', path, token, customHeaders }); + return send({ method: 'DELETE', path, token, customHeaders }); } export function postRequest(path: string, data: any, token: string, customHeaders?: Record) { - return send({ method: 'POST', path, data, token, customHeaders }); + return send({ method: 'POST', path, data, token, customHeaders }); } export function putRequest(path: string, data: any, token: string, customHeaders?: Record) { - return send({ method: 'PUT', path, data, token, customHeaders }); -} \ No newline at end of file + return send({ method: 'PUT', path, data, token, customHeaders }); +} diff --git a/web/src/lib/components/asset-viewer/video-viewer.svelte b/web/src/lib/components/asset-viewer/video-viewer.svelte index 1c775ab8b1855..3697bc9acb287 100644 --- a/web/src/lib/components/asset-viewer/video-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-viewer.svelte @@ -67,7 +67,7 @@ {#if isVideoLoading} -
+
{/if} diff --git a/web/src/lib/components/shared/announcement-box.svelte b/web/src/lib/components/shared/announcement-box.svelte new file mode 100644 index 0000000000000..e0096daeb8555 --- /dev/null +++ b/web/src/lib/components/shared/announcement-box.svelte @@ -0,0 +1,61 @@ + + +
+ console.log('Click outside')}> +
+

🎉 NEW VERSION AVAILABLE 🎉

+
+ +
+
+ Hi friend, there is a new release of IMMICH, please take your time to visit the + release note + 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 application automatically. +
+ + {#if remoteVersion == 'v1.11.0_17-dev'} +
+ This specific version v1.11.0_17-dev includes changes in the docker-compose + setup that added additional containters. Please make sure to update the docker-compose file, pull new images + and check your setup for the latest features and bug fixes. +
+ {/if} +
+ +
Your friend, Alex
+
+ Local Version {localVersion} +
+ Remote Version {remoteVersion} +
+ +
+ +
+
+
+
diff --git a/web/src/lib/components/shared/full-screen-modal.svelte b/web/src/lib/components/shared/full-screen-modal.svelte index 22ca352b28055..7a6866a3c3e84 100644 --- a/web/src/lib/components/shared/full-screen-modal.svelte +++ b/web/src/lib/components/shared/full-screen-modal.svelte @@ -11,7 +11,7 @@ out:fade={{ duration: 100 }} class="absolute w-full h-full bg-black/40 z-[100] flex place-items-center place-content-center " > -
dispatch('clickOutside')}> +
dispatch('clickOutside')}>
diff --git a/web/src/lib/components/shared/navigation-bar.svelte b/web/src/lib/components/shared/navigation-bar.svelte index af253c7c053e8..22f9c518c9f75 100644 --- a/web/src/lib/components/shared/navigation-bar.svelte +++ b/web/src/lib/components/shared/navigation-bar.svelte @@ -22,7 +22,6 @@ }; const navigateToAdmin = () => { - console.log('Navigating to admin page'); goto('/admin'); }; diff --git a/web/src/lib/components/shared/status-box.svelte b/web/src/lib/components/shared/status-box.svelte index e8b95217fc6bc..d1af9468e9f9b 100644 --- a/web/src/lib/components/shared/status-box.svelte +++ b/web/src/lib/components/shared/status-box.svelte @@ -5,6 +5,7 @@ import Cloud from 'svelte-material-icons/Cloud.svelte'; import Dns from 'svelte-material-icons/Dns.svelte'; import LoadingSpinner from './loading-spinner.svelte'; + import { goto } from '$app/navigation'; type ServerInfoType = { diskAvailable: string; @@ -77,7 +78,11 @@

Server

- +

Status

@@ -94,4 +99,10 @@
+
diff --git a/web/src/lib/utils/check-app-version.ts b/web/src/lib/utils/check-app-version.ts new file mode 100644 index 0000000000000..3a793db147bd8 --- /dev/null +++ b/web/src/lib/utils/check-app-version.ts @@ -0,0 +1,50 @@ +type CheckAppVersionReponse = { + shouldShowAnnouncement: boolean; + localVersion?: string; + remoteVersion?: string; +}; + +type GithubRelease = { + tag_name: string; +}; + +export const checkAppVersion = async (): Promise => { + const res = await fetch('https://api.github.com/repos/alextran1502/immich/releases/latest', { + headers: { + Accept: 'application/vnd.github.v3+json', + }, + }); + + if (res.status == 200) { + const latestRelease = (await res.json()) as GithubRelease; + const appVersion = localStorage.getItem('appVersion'); + + if (!appVersion) { + return { + shouldShowAnnouncement: true, + remoteVersion: latestRelease.tag_name, + localVersion: 'empty', + }; + } + + if (appVersion != latestRelease.tag_name) { + return { + shouldShowAnnouncement: true, + remoteVersion: latestRelease.tag_name, + localVersion: appVersion, + }; + } + + return { + shouldShowAnnouncement: false, + remoteVersion: latestRelease.tag_name, + localVersion: appVersion, + }; + } else { + return { + shouldShowAnnouncement: false, + remoteVersion: '0', + localVersion: '0', + }; + } +}; diff --git a/web/src/routes/__layout.svelte b/web/src/routes/__layout.svelte index 846a259ac3374..d81a21e70415b 100644 --- a/web/src/routes/__layout.svelte +++ b/web/src/routes/__layout.svelte @@ -1,7 +1,19 @@
@@ -19,6 +36,10 @@
+ + {#if shouldShowAnnouncement} + + {/if}
{/key}
diff --git a/web/src/routes/auth/login/api/get-users.ts b/web/src/routes/auth/login/api/get-users.ts index 2cbedb3accd0a..3f48d1638e36c 100644 --- a/web/src/routes/auth/login/api/get-users.ts +++ b/web/src/routes/auth/login/api/get-users.ts @@ -1,12 +1,11 @@ import type { RequestHandler } from '@sveltejs/kit'; import { getRequest } from '../../../../lib/api'; - export const get: RequestHandler = async ({ request, locals }) => { - const allUsers = await getRequest('user?isAll=true', locals.user!.accessToken) + const allUsers = await getRequest('user?isAll=true', locals.user!.accessToken); - return { - status: 200, - body: { allUsers } - }; -} \ No newline at end of file + return { + status: 200, + body: { allUsers }, + }; +}; diff --git a/web/src/routes/auth/login/update.ts b/web/src/routes/auth/login/update.ts index 72228346b6e8a..661826c876bf5 100644 --- a/web/src/routes/auth/login/update.ts +++ b/web/src/routes/auth/login/update.ts @@ -1,63 +1,63 @@ import type { RequestHandler } from '@sveltejs/kit'; import { putRequest } from '../../../lib/api'; -import * as cookie from 'cookie' - +import * as cookie from 'cookie'; export const post: RequestHandler = async ({ request, locals }) => { - - const form = await request.formData(); - - const firstName = form.get('firstName') - const lastName = form.get('lastName') - - if (locals.user) { - const updatedUser = await putRequest('user', { - id: locals.user.id, - firstName, - lastName - }, locals.user.accessToken) - - - - - return { - status: 200, - body: { - user: { - id: updatedUser.id, - accessToken: locals.user.accessToken, - firstName: updatedUser.firstName, - lastName: updatedUser.lastName, - isAdmin: updatedUser.isAdmin, - email: updatedUser.email, - }, - success: 'Update user success' - }, - headers: { - 'Set-Cookie': cookie.serialize('session', JSON.stringify( - { - id: updatedUser.id, - accessToken: locals.user.accessToken, - firstName: updatedUser.firstName, - lastName: updatedUser.lastName, - isAdmin: updatedUser.isAdmin, - email: updatedUser.email, - }), { - path: '/', - httpOnly: true, - sameSite: 'strict', - maxAge: 60 * 60 * 24 * 30, - }) - } - } - } - - - - return { - status: 400, - body: { - error: 'Cannot get access token from cookies' - } - } -} \ No newline at end of file + const form = await request.formData(); + + const firstName = form.get('firstName'); + const lastName = form.get('lastName'); + + if (locals.user) { + const updatedUser = await putRequest( + 'user', + { + id: locals.user.id, + firstName, + lastName, + }, + locals.user.accessToken, + ); + + return { + status: 200, + body: { + user: { + id: updatedUser.id, + accessToken: locals.user.accessToken, + firstName: updatedUser.firstName, + lastName: updatedUser.lastName, + isAdmin: updatedUser.isAdmin, + email: updatedUser.email, + }, + success: 'Update user success', + }, + headers: { + 'Set-Cookie': cookie.serialize( + 'session', + JSON.stringify({ + id: updatedUser.id, + accessToken: locals.user.accessToken, + firstName: updatedUser.firstName, + lastName: updatedUser.lastName, + isAdmin: updatedUser.isAdmin, + email: updatedUser.email, + }), + { + path: '/', + httpOnly: true, + sameSite: 'strict', + maxAge: 60 * 60 * 24 * 30, + }, + ), + }, + }; + } + + return { + status: 400, + body: { + error: 'Cannot get access token from cookies', + }, + }; +}; diff --git a/web/src/routes/photos/index.svelte b/web/src/routes/photos/index.svelte index 8c957ad5dd7f2..4303490023bce 100644 --- a/web/src/routes/photos/index.svelte +++ b/web/src/routes/photos/index.svelte @@ -3,6 +3,7 @@ import type { Load } from '@sveltejs/kit'; import { getAssetsInfo } from '$lib/stores/assets'; + import { checkAppVersion } from '$lib/utils/check-app-version'; export const load: Load = async ({ session }) => { if (!session.user) {