Compare commits
15 Commits
v1.3.1-dev
...
v1.4.0+7-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfc5229964 | ||
|
|
f9ddeac265 | ||
|
|
8d7c576037 | ||
|
|
fccdbdd66a | ||
|
|
23ba651705 | ||
|
|
ac0ad98b55 | ||
|
|
9cbd5d1b0c | ||
|
|
80fd664cc8 | ||
|
|
041c711cb9 | ||
|
|
dd9c5244fd | ||
|
|
fe693db84f | ||
|
|
5c9d3cd08b | ||
|
|
725ab5622f | ||
|
|
e9acd21733 | ||
|
|
ce1ab1ed50 |
@@ -1,10 +1,6 @@
|
|||||||
name: Build Server
|
name: Build Server - Latest
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Triggers the workflow on push or pull request events but only for the main branch
|
|
||||||
#schedule:
|
|
||||||
# * is a special character in YAML so you have to quote this string
|
|
||||||
#- cron: '0 0 * * *'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
40
.github/workflows/build_push_server_release.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Build Server - Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
buildandpush:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: "main" # branch
|
||||||
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1.2.0
|
||||||
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1.6.0
|
||||||
|
# https://github.com/docker/login-action#docker-hub
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
|
- name: Build and push Immich
|
||||||
|
uses: docker/build-push-action@v2.10.0
|
||||||
|
with:
|
||||||
|
context: ./server
|
||||||
|
file: ./server/Dockerfile
|
||||||
|
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
||||||
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
|
pull: true
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-server:${{github.ref_name}}
|
||||||
2
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
9
Makefile
@@ -1,8 +1,11 @@
|
|||||||
dev:
|
dev:
|
||||||
docker-compose -f ./docker/docker-compose.yml up
|
docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
dev-update:
|
dev-update:
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V
|
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
dev-scale:
|
dev-scale:
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich_server=3 --remove-orphans
|
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich_server=3 --remove-orphans
|
||||||
|
|
||||||
|
prod:
|
||||||
|
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
|
||||||
@@ -34,7 +34,8 @@ Loading ~4000 images/videos
|
|||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="design/nsc1.png" width="150" title="Login With Custom URL">
|
<img src="design/nsc1.png" width="150" title="Login With Custom URL">
|
||||||
<img src="design/nsc2.png" width="150" title="Backup Setting Info">
|
<img src="design/nsc2.png" width="150" title="Backup Setting Info">
|
||||||
<img src="design/nsc3.png" width="150" title="Multiple seelct">
|
<img src="design/nsc3.png" width="150" title="Multiple select">
|
||||||
|
<img src="design/nsc4.jpeg" width="150" title="Curated Search Info">
|
||||||
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
@@ -55,11 +56,13 @@ This project is under heavy development, there will be continous functions, feat
|
|||||||
- Extract and display EXIF info.
|
- Extract and display EXIF info.
|
||||||
- Real-time render from multi-device upload event.
|
- Real-time render from multi-device upload event.
|
||||||
- Image Tagging/Classification based on ImageNet dataset
|
- Image Tagging/Classification based on ImageNet dataset
|
||||||
|
- Object detection based on COCO SSD.
|
||||||
- Search assets based on tags and exif data (lens, make, model, orientation)
|
- Search assets based on tags and exif data (lens, make, model, orientation)
|
||||||
- Upload assets from your local computer/server using [immich cli tools](https://www.npmjs.com/package/immich)
|
- Upload assets from your local computer/server using [immich cli tools](https://www.npmjs.com/package/immich)
|
||||||
- [Optional] Reserve geocoding using Mapbox (Generous free-tier of 100,000 search/month)
|
- [Optional] Reserve geocoding using Mapbox (Generous free-tier of 100,000 search/month)
|
||||||
- Show asset's location information on map (OpenStreetMap).
|
- Show asset's location information on map (OpenStreetMap).
|
||||||
- Show curated places on the search page
|
- Show curated places on the search page
|
||||||
|
- Show curated objects on the search page
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
@@ -69,7 +72,7 @@ You can use docker compose for development, there are several services that comp
|
|||||||
2. PostgreSQL
|
2. PostgreSQL
|
||||||
3. Redis
|
3. Redis
|
||||||
4. Nginx
|
4. Nginx
|
||||||
5. TensorFlow and Keras
|
5. TensorFlow
|
||||||
|
|
||||||
## Populate .env file
|
## Populate .env file
|
||||||
|
|
||||||
|
|||||||
BIN
design/nsc4.jpeg
Normal file
|
After Width: | Height: | Size: 406 KiB |
@@ -1,6 +1,3 @@
|
|||||||
# STAGE
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
|
|||||||
89
docker/docker-compose.dev.yml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
immich_server:
|
||||||
|
image: immich-server-dev:1.3.2
|
||||||
|
build:
|
||||||
|
context: ../server
|
||||||
|
dockerfile: ../server/Dockerfile
|
||||||
|
command: npm run start:dev
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
volumes:
|
||||||
|
- ../server:/usr/src/app
|
||||||
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- database
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
|
||||||
|
immich_microservices:
|
||||||
|
image: immich-microservices-dev:1.3.2
|
||||||
|
build:
|
||||||
|
context: ../microservices
|
||||||
|
dockerfile: ../microservices/Dockerfile
|
||||||
|
command: npm run start:dev
|
||||||
|
expose:
|
||||||
|
- "3001"
|
||||||
|
volumes:
|
||||||
|
- ../microservices:/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
|
||||||
|
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: immich_redis
|
||||||
|
image: redis:6.2
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
|
||||||
|
database:
|
||||||
|
container_name: immich_postgres
|
||||||
|
image: postgres:14
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||||
|
PG_DATA: /var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
container_name: proxy_nginx
|
||||||
|
image: nginx:latest
|
||||||
|
volumes:
|
||||||
|
- ./settings/nginx-conf:/etc/nginx/conf.d
|
||||||
|
ports:
|
||||||
|
- 2283:80
|
||||||
|
- 2284:443
|
||||||
|
logging:
|
||||||
|
driver: none
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
depends_on:
|
||||||
|
- immich_server
|
||||||
|
|
||||||
|
networks:
|
||||||
|
immich_network:
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
@@ -2,10 +2,9 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich_server:
|
||||||
image: immich-server-dev:1.3.0
|
image: immich-server-dev:1.4.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
target: development
|
|
||||||
dockerfile: ../server/Dockerfile
|
dockerfile: ../server/Dockerfile
|
||||||
command: npm run start:dev
|
command: npm run start:dev
|
||||||
expose:
|
expose:
|
||||||
@@ -22,6 +21,33 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich_network
|
||||||
|
|
||||||
|
immich_microservices:
|
||||||
|
image: immich-microservices-dev:1.4.0
|
||||||
|
build:
|
||||||
|
context: ../microservices
|
||||||
|
dockerfile: ../microservices/Dockerfile
|
||||||
|
command: npm run start:dev
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [ gpu ]
|
||||||
|
expose:
|
||||||
|
- "3001"
|
||||||
|
volumes:
|
||||||
|
- ../microservices:/usr/src/app
|
||||||
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
- immich_server
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
@@ -60,35 +86,6 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- immich_server
|
- immich_server
|
||||||
|
|
||||||
immich_tf_fastapi:
|
|
||||||
container_name: immich_tf_fastapi
|
|
||||||
image: tensor_flow_fastapi:1.0.0
|
|
||||||
restart: always
|
|
||||||
command: uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port 8000 --reload
|
|
||||||
build:
|
|
||||||
context: ../machine_learning
|
|
||||||
target: gpu
|
|
||||||
dockerfile: ../machine_learning/Dockerfile
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
count: 1
|
|
||||||
capabilities: [gpu]
|
|
||||||
volumes:
|
|
||||||
- ../machine_learning/app:/code/app
|
|
||||||
- ${UPLOAD_LOCATION}:/code/app/upload
|
|
||||||
ports:
|
|
||||||
- 2285:8000
|
|
||||||
expose:
|
|
||||||
- "8000"
|
|
||||||
depends_on:
|
|
||||||
- database
|
|
||||||
networks:
|
|
||||||
- immich_network
|
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
immich_network:
|
immich_network:
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich_server:
|
||||||
image: immich-server-dev:1.3.0
|
image: immich-server:1.4.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
target: development
|
|
||||||
dockerfile: ../server/Dockerfile
|
dockerfile: ../server/Dockerfile
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
# command: npm run start:dev
|
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -17,12 +15,36 @@ services:
|
|||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich_network
|
||||||
|
|
||||||
|
immich_microservices:
|
||||||
|
image: immich-microservices:1.4.0
|
||||||
|
build:
|
||||||
|
context: ../microservices
|
||||||
|
dockerfile: ../microservices/Dockerfile
|
||||||
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
|
expose:
|
||||||
|
- "3001"
|
||||||
|
volumes:
|
||||||
|
- ../microservices:/usr/src/app
|
||||||
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
@@ -61,26 +83,26 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- immich_server
|
- immich_server
|
||||||
|
|
||||||
immich_tf_fastapi:
|
# immich_tf_fastapi:
|
||||||
container_name: immich_tf_fastapi
|
# container_name: immich_tf_fastapi
|
||||||
image: tensor_flow_fastapi:1.0.0
|
# image: tensor_flow_fastapi:1.0.0
|
||||||
restart: always
|
# restart: always
|
||||||
command: uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port 8000 --reload
|
# command: uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port 8000 --reload
|
||||||
build:
|
# build:
|
||||||
context: ../machine_learning
|
# context: ../machine_learning
|
||||||
target: cpu
|
# target: cpu
|
||||||
dockerfile: ../machine_learning/Dockerfile
|
# dockerfile: ../machine_learning/Dockerfile
|
||||||
volumes:
|
# volumes:
|
||||||
- ../machine_learning/app:/code/app
|
# - ../machine_learning/app:/code/app
|
||||||
- ${UPLOAD_LOCATION}:/code/app/upload
|
# - ${UPLOAD_LOCATION}:/code/app/upload
|
||||||
ports:
|
# ports:
|
||||||
- 2285:8000
|
# - 2285:8000
|
||||||
expose:
|
# expose:
|
||||||
- "8000"
|
# - "8000"
|
||||||
depends_on:
|
# depends_on:
|
||||||
- database
|
# - database
|
||||||
networks:
|
# networks:
|
||||||
- immich_network
|
# - immich_network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
immich_network:
|
immich_network:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ server {
|
|||||||
client_max_body_size 50000M;
|
client_max_body_size 50000M;
|
||||||
|
|
||||||
listen 80;
|
listen 80;
|
||||||
|
access_log off;
|
||||||
location / {
|
location / {
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
proxy_buffer_size 16k;
|
proxy_buffer_size 16k;
|
||||||
|
|||||||
3
fastlane/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
This directory exists because of the F-Droid build process. F-Droid is using the same directory structure as Fastlane for the app metadata.
|
||||||
|
|
||||||
|
Because F-Droid expects the metadata to be located in the root of the repository we need to have this symlink.
|
||||||
1
fastlane/metadata
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../mobile/android/fastlane/metadata
|
||||||
4
microservices/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
upload/
|
||||||
|
dist/
|
||||||
|
|
||||||
24
microservices/.eslintrc.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
35
microservices/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
4
microservices/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
16
microservices/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:16-bullseye-slim
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
4
microservices/README.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
# Microservices for Immich
|
||||||
|
|
||||||
|
## Image Classifier
|
||||||
2
microservices/entrypoint.sh
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# npm run typeorm migration:run
|
||||||
|
npm run build && npm run start:prod
|
||||||
4
microservices/nest-cli.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src"
|
||||||
|
}
|
||||||
17323
microservices/package-lock.json
generated
Normal file
83
microservices/package.json
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"name": "nest_microservices",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"prebuild": "rimraf dist",
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^8.0.0",
|
||||||
|
"@nestjs/core": "^8.0.0",
|
||||||
|
"@nestjs/mapped-types": "^1.0.1",
|
||||||
|
"@nestjs/platform-express": "^8.0.0",
|
||||||
|
"@nestjs/typeorm": "^8.0.3",
|
||||||
|
"@tensorflow-models/coco-ssd": "^2.2.2",
|
||||||
|
"@tensorflow-models/mobilenet": "^2.1.0",
|
||||||
|
"@tensorflow/tfjs": "^3.15.0",
|
||||||
|
"@tensorflow/tfjs-converter": "^3.15.0",
|
||||||
|
"@tensorflow/tfjs-core": "^3.15.0",
|
||||||
|
"@tensorflow/tfjs-node": "^3.15.0",
|
||||||
|
"@tensorflow/tfjs-node-gpu": "^3.15.0",
|
||||||
|
"@trpc/server": "^9.20.3",
|
||||||
|
"pg": "^8.7.3",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"rxjs": "^7.2.0",
|
||||||
|
"typeorm": "^0.2.45"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^8.2.4",
|
||||||
|
"@nestjs/schematics": "^8.0.0",
|
||||||
|
"@nestjs/testing": "^8.0.0",
|
||||||
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/jest": "27.4.1",
|
||||||
|
"@types/node": "^16.0.0",
|
||||||
|
"@types/supertest": "^2.0.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
|
"eslint": "^8.0.1",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"jest": "^27.2.5",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"source-map-support": "^0.5.20",
|
||||||
|
"supertest": "^6.1.3",
|
||||||
|
"ts-jest": "^27.0.3",
|
||||||
|
"ts-loader": "^9.2.3",
|
||||||
|
"ts-node": "^10.0.0",
|
||||||
|
"tsconfig-paths": "^3.10.1",
|
||||||
|
"typescript": "^4.3.5"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
microservices/src/app.module.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ImageClassifierModule } from './image-classifier/image-classifier.module';
|
||||||
|
import { databaseConfig } from './config/database.config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ObjectDetectionModule } from './object-detection/object-detection.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forRoot(databaseConfig),
|
||||||
|
ImageClassifierModule,
|
||||||
|
ObjectDetectionModule,
|
||||||
|
],
|
||||||
|
controllers: [],
|
||||||
|
providers: [],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
11
microservices/src/config/database.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
export const databaseConfig: TypeOrmModuleOptions = {
|
||||||
|
type: 'postgres',
|
||||||
|
host: 'immich_postgres',
|
||||||
|
port: 5432,
|
||||||
|
username: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_DATABASE_NAME,
|
||||||
|
synchronize: false,
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Body, Controller, Post } from '@nestjs/common';
|
||||||
|
import { ImageClassifierService } from './image-classifier.service';
|
||||||
|
|
||||||
|
@Controller('image-classifier')
|
||||||
|
export class ImageClassifierController {
|
||||||
|
constructor(
|
||||||
|
private readonly imageClassifierService: ImageClassifierService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('/tagImage')
|
||||||
|
async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
|
||||||
|
return await this.imageClassifierService.tagImage(thumbnailPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ImageClassifierService } from './image-classifier.service';
|
||||||
|
import { ImageClassifierController } from './image-classifier.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ImageClassifierController],
|
||||||
|
providers: [ImageClassifierService],
|
||||||
|
})
|
||||||
|
export class ImageClassifierModule {}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as mobilenet from '@tensorflow-models/mobilenet';
|
||||||
|
import * as cocoSsd from '@tensorflow-models/coco-ssd';
|
||||||
|
import * as tf from '@tensorflow/tfjs-node';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ImageClassifierService {
|
||||||
|
private readonly MOBILENET_VERSION = 2;
|
||||||
|
private readonly MOBILENET_ALPHA = 1.0;
|
||||||
|
|
||||||
|
private mobileNetModel: mobilenet.MobileNet;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
Logger.log(
|
||||||
|
`Running Node TensorFlow Version : ${tf.version['tfjs']}`,
|
||||||
|
'ImageClassifier',
|
||||||
|
);
|
||||||
|
mobilenet
|
||||||
|
.load({
|
||||||
|
version: this.MOBILENET_VERSION,
|
||||||
|
alpha: this.MOBILENET_ALPHA,
|
||||||
|
})
|
||||||
|
.then((mobilenetModel) => (this.mobileNetModel = mobilenetModel));
|
||||||
|
}
|
||||||
|
|
||||||
|
async tagImage(thumbnailPath: string) {
|
||||||
|
try {
|
||||||
|
const isExist = fs.existsSync(thumbnailPath);
|
||||||
|
if (isExist) {
|
||||||
|
const tags = [];
|
||||||
|
const image = fs.readFileSync(thumbnailPath);
|
||||||
|
const decodedImage = tf.node.decodeImage(image, 3) as tf.Tensor3D;
|
||||||
|
const predictions = await this.mobileNetModel.classify(decodedImage);
|
||||||
|
|
||||||
|
for (const prediction of predictions) {
|
||||||
|
if (prediction.probability >= 0.1) {
|
||||||
|
tags.push(...prediction.className.split(',').map((e) => e.trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error reading file ', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
microservices/src/main.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
await app.listen(3001, () => {
|
||||||
|
if (process.env.NODE_ENV == 'development') {
|
||||||
|
Logger.log(
|
||||||
|
'Running Immich Microservices in DEVELOPMENT environment',
|
||||||
|
'IMMICH MICROSERVICES',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV == 'production') {
|
||||||
|
Logger.log(
|
||||||
|
'Running Immich Microservices in PRODUCTION environment',
|
||||||
|
'IMMICH MICROSERVICES',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Body, Controller, Post } from '@nestjs/common';
|
||||||
|
import { ObjectDetectionService } from './object-detection.service';
|
||||||
|
|
||||||
|
@Controller('object-detection')
|
||||||
|
export class ObjectDetectionController {
|
||||||
|
constructor(
|
||||||
|
private readonly objectDetectionService: ObjectDetectionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('/detectObject')
|
||||||
|
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
|
||||||
|
return await this.objectDetectionService.detectObject(thumbnailPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ObjectDetectionService } from './object-detection.service';
|
||||||
|
import { ObjectDetectionController } from './object-detection.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ObjectDetectionController],
|
||||||
|
providers: [ObjectDetectionService],
|
||||||
|
})
|
||||||
|
export class ObjectDetectionModule {}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as cocoSsd from '@tensorflow-models/coco-ssd';
|
||||||
|
import * as tf from '@tensorflow/tfjs-node';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ObjectDetectionService {
|
||||||
|
private cocoSsdModel: cocoSsd.ObjectDetection;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
Logger.log(
|
||||||
|
`Running Node TensorFlow Version : ${tf.version['tfjs']}`,
|
||||||
|
'ObjectDetection',
|
||||||
|
);
|
||||||
|
cocoSsd.load().then((model) => (this.cocoSsdModel = model));
|
||||||
|
}
|
||||||
|
async detectObject(thumbnailPath: string) {
|
||||||
|
try {
|
||||||
|
const isExist = fs.existsSync(thumbnailPath);
|
||||||
|
if (isExist) {
|
||||||
|
const tags = new Set();
|
||||||
|
const image = fs.readFileSync(thumbnailPath);
|
||||||
|
const decodedImage = tf.node.decodeImage(image, 3) as tf.Tensor3D;
|
||||||
|
const predictions = await this.cocoSsdModel.detect(decodedImage);
|
||||||
|
|
||||||
|
for (const result of predictions) {
|
||||||
|
if (result.score > 0.5) {
|
||||||
|
tags.add(result.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...tags];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error reading file ', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
microservices/test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { AppModule } from '../src/app.module';
|
||||||
|
|
||||||
|
// End to End test
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
9
microservices/test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
microservices/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
21
microservices/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "es2017",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Added curated locations and objects on the search page
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
This is a client app for the self-hostable Immich Server (which can be found with the app's source repo). You will need to run/manage the server on your own in order to use the app.
|
||||||
|
|
||||||
|
Once set up, this app can be used as photo and video backup solution directly from your mobile phone.
|
||||||
|
|
||||||
|
<b>Features:</b>
|
||||||
|
|
||||||
|
* Upload and view assets(videos/images).
|
||||||
|
* Multi-user supported.
|
||||||
|
* Quick navigation with drag scroll bar.
|
||||||
|
* Auto Backup.
|
||||||
|
* Support HEIC/HEIF Backup.
|
||||||
|
* Extract and display EXIF info.
|
||||||
|
* Real-time render from multi-device upload event.
|
||||||
|
* Image Tagging/Classification based on ImageNet dataset
|
||||||
|
* Object detection based on COCO SSD.
|
||||||
|
* Search assets based on tags and exif data (lens, make, model, orientation)
|
||||||
|
* Upload assets from your local computer/server using <a href='https://www.npmjs.com/package/immich' target='_blank' rel='nofollow'>immich cli tools</a>
|
||||||
|
* [Optional] Reserve geocoding using Mapbox (Generous free-tier of 100,000 search/month)
|
||||||
|
* Show asset's location information on map (OpenStreetMap).
|
||||||
|
* Show curated places on the search page
|
||||||
|
* Show curated objects on the search page
|
||||||
|
After Width: | Height: | Size: 48 KiB |
BIN
mobile/android/fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 517 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1 @@
|
|||||||
|
This is a client app for the self-hostable Immich Server (which can be found with the app's source repo). You will need to run/manage the server on your own in order to use the app.
|
||||||
1
mobile/android/fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Immich
|
||||||
@@ -4,3 +4,4 @@ distributionPath=wrapper/dists
|
|||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||||
|
distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583
|
||||||
@@ -79,4 +79,4 @@ SPEC CHECKSUMS:
|
|||||||
|
|
||||||
PODFILE CHECKSUM: 05c3056158482c567a3e0cdab1351ceeee238a07
|
PODFILE CHECKSUM: 05c3056158482c567a3e0cdab1351ceeee238a07
|
||||||
|
|
||||||
COCOAPODS: 1.10.1
|
COCOAPODS: 1.11.3
|
||||||
|
|||||||
@@ -341,7 +341,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
NEW_SETTING = "";
|
NEW_SETTING = "";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -425,7 +425,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
NEW_SETTING = "";
|
NEW_SETTING = "";
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
@@ -475,7 +475,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
NEW_SETTING = "";
|
NEW_SETTING = "";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ default_platform(:ios)
|
|||||||
platform :ios do
|
platform :ios do
|
||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
|
increment_version_number(
|
||||||
|
version_number: "1.4.0"
|
||||||
|
)
|
||||||
increment_build_number({
|
increment_build_number({
|
||||||
build_number: latest_testflight_build_number + 1
|
build_number: 0
|
||||||
})
|
})
|
||||||
build_app(scheme: "Runner",
|
build_app(scheme: "Runner",
|
||||||
workspace: "Runner.xcworkspace",
|
workspace: "Runner.xcworkspace",
|
||||||
@@ -29,19 +32,4 @@ platform :ios do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "iOS Release"
|
|
||||||
lane :release do
|
|
||||||
increment_version_number(
|
|
||||||
version_number: "1.3.0"
|
|
||||||
)
|
|
||||||
increment_build_number({
|
|
||||||
build_number: latest_testflight_build_number + 1
|
|
||||||
})
|
|
||||||
build_app(scheme: "Runner",
|
|
||||||
workspace: "Runner.xcworkspace",
|
|
||||||
xcargs: "-allowProvisioningUpdates")
|
|
||||||
upload_to_testflight(
|
|
||||||
skip_waiting_for_build_processing: true
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final usernameController = useTextEditingController(text: 'testuser@email.com');
|
final usernameController = useTextEditingController(text: 'testuser@email.com');
|
||||||
final passwordController = useTextEditingController(text: 'password');
|
final passwordController = useTextEditingController(text: 'password');
|
||||||
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.103:2283');
|
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.216:2283');
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
@@ -78,7 +78,7 @@ class EmailInput extends StatelessWidget {
|
|||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration:
|
decoration:
|
||||||
const InputDecoration(labelText: 'email', border: OutlineInputBorder(), hintText: 'youremail@email.com'),
|
const InputDecoration(labelText: 'Email', border: OutlineInputBorder(), hintText: 'youremail@email.com'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
mobile/lib/modules/search/models/curated_object.model.dart
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class CuratedObject {
|
||||||
|
final String id;
|
||||||
|
final String object;
|
||||||
|
final String resizePath;
|
||||||
|
final String deviceAssetId;
|
||||||
|
final String deviceId;
|
||||||
|
CuratedObject({
|
||||||
|
required this.id,
|
||||||
|
required this.object,
|
||||||
|
required this.resizePath,
|
||||||
|
required this.deviceAssetId,
|
||||||
|
required this.deviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
CuratedObject copyWith({
|
||||||
|
String? id,
|
||||||
|
String? object,
|
||||||
|
String? resizePath,
|
||||||
|
String? deviceAssetId,
|
||||||
|
String? deviceId,
|
||||||
|
}) {
|
||||||
|
return CuratedObject(
|
||||||
|
id: id ?? this.id,
|
||||||
|
object: object ?? this.object,
|
||||||
|
resizePath: resizePath ?? this.resizePath,
|
||||||
|
deviceAssetId: deviceAssetId ?? this.deviceAssetId,
|
||||||
|
deviceId: deviceId ?? this.deviceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
result.addAll({'id': id});
|
||||||
|
result.addAll({'object': object});
|
||||||
|
result.addAll({'resizePath': resizePath});
|
||||||
|
result.addAll({'deviceAssetId': deviceAssetId});
|
||||||
|
result.addAll({'deviceId': deviceId});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory CuratedObject.fromMap(Map<String, dynamic> map) {
|
||||||
|
return CuratedObject(
|
||||||
|
id: map['id'] ?? '',
|
||||||
|
object: map['object'] ?? '',
|
||||||
|
resizePath: map['resizePath'] ?? '',
|
||||||
|
deviceAssetId: map['deviceAssetId'] ?? '',
|
||||||
|
deviceId: map['deviceId'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory CuratedObject.fromJson(String source) => CuratedObject.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CuratedObject(id: $id, object: $object, resizePath: $resizePath, deviceAssetId: $deviceAssetId, deviceId: $deviceId)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is CuratedObject &&
|
||||||
|
other.id == id &&
|
||||||
|
other.object == object &&
|
||||||
|
other.resizePath == resizePath &&
|
||||||
|
other.deviceAssetId == deviceAssetId &&
|
||||||
|
other.deviceId == deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^ object.hashCode ^ resizePath.hashCode ^ deviceAssetId.hashCode ^ deviceId.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/search_page_state.model.dart';
|
import 'package:immich_mobile/modules/search/models/search_page_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
@@ -64,3 +65,14 @@ final getCuratedLocationProvider = FutureProvider.autoDispose<List<CuratedLocati
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final getCuratedObjectProvider = FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
|
||||||
|
final SearchService _searchService = SearchService();
|
||||||
|
|
||||||
|
var curatedObject = await _searchService.getCuratedObjects();
|
||||||
|
if (curatedObject != null) {
|
||||||
|
return curatedObject;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||||
|
|
||||||
@@ -52,4 +53,19 @@ class SearchService {
|
|||||||
throw Error();
|
throw Error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<CuratedObject>?> getCuratedObjects() async {
|
||||||
|
try {
|
||||||
|
var res = await _networkService.getRequest(url: "asset/allObjects");
|
||||||
|
|
||||||
|
List<dynamic> decodedData = jsonDecode(res.toString());
|
||||||
|
|
||||||
|
List<CuratedObject> result = List.from(decodedData.map((a) => CuratedObject.fromMap(a)));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("[ERROR] [CuratedObject] ${e.toString()}");
|
||||||
|
throw Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import 'package:hive_flutter/hive_flutter.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
|
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class SearchPage extends HookConsumerWidget {
|
class SearchPage extends HookConsumerWidget {
|
||||||
@@ -22,6 +24,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
||||||
AsyncValue<List<CuratedLocation>> curatedLocation = ref.watch(getCuratedLocationProvider);
|
AsyncValue<List<CuratedLocation>> curatedLocation = ref.watch(getCuratedLocationProvider);
|
||||||
|
AsyncValue<List<CuratedObject>> curatedObjects = ref.watch(getCuratedObjectProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
searchFocusNode = FocusNode();
|
searchFocusNode = FocusNode();
|
||||||
@@ -82,6 +85,54 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildThings() {
|
||||||
|
return curatedObjects.when(
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
error: (err, stack) => Text('Error: $err'),
|
||||||
|
data: (objects) {
|
||||||
|
return objects.isNotEmpty
|
||||||
|
? SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.width / 3,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(left: 16),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: curatedObjects.value?.length,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
CuratedObject curatedObjectInfo = objects[index];
|
||||||
|
var thumbnailRequestUrl =
|
||||||
|
'${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true';
|
||||||
|
|
||||||
|
return ThumbnailWithInfo(
|
||||||
|
imageUrl: thumbnailRequestUrl,
|
||||||
|
textInfo: curatedObjectInfo.object,
|
||||||
|
onTap: () {
|
||||||
|
AutoRouter.of(context)
|
||||||
|
.push(SearchResultRoute(searchTerm: curatedObjectInfo.object.capitalizeFirstLetter()));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.width / 3,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(left: 16),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: 1,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
return ThumbnailWithInfo(
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
|
||||||
|
textInfo: 'No Object Info Available',
|
||||||
|
onTap: () {},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: SearchBar(
|
appBar: SearchBar(
|
||||||
searchFocusNode: searchFocusNode,
|
searchFocusNode: searchFocusNode,
|
||||||
@@ -104,6 +155,14 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildPlaces(),
|
_buildPlaces(),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
"Things",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildThings()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
isSearchEnabled ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(),
|
isSearchEnabled ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(),
|
||||||
@@ -160,7 +219,7 @@ class ThumbnailWithInfo extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: MediaQuery.of(context).size.width / 3,
|
width: MediaQuery.of(context).size.width / 3,
|
||||||
child: Text(
|
child: Text(
|
||||||
textInfo,
|
textInfo.capitalizeFirstLetter(),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class TabNavigationObserver extends AutoRouterObserver {
|
|||||||
if (route.name == 'SearchRoute') {
|
if (route.name == 'SearchRoute') {
|
||||||
// Refresh Location State
|
// Refresh Location State
|
||||||
ref.refresh(getCuratedLocationProvider);
|
ref.refresh(getCuratedLocationProvider);
|
||||||
|
ref.refresh(getCuratedObjectProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Total",
|
title: "Total",
|
||||||
subtitle: "All images and video on the device",
|
subtitle: "All images and videos on the device",
|
||||||
info: "${_backupState.totalAssetCount}",
|
info: "${_backupState.totalAssetCount}",
|
||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
|
|||||||
5
mobile/lib/utils/capitalize_first_letter.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
extension StringExtension on String {
|
||||||
|
String capitalizeFirstLetter() {
|
||||||
|
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.3.0+0
|
version: 1.4.0+6
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.15.1 <3.0.0"
|
sdk: ">=2.15.1 <3.0.0"
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
##################################
|
FROM node:16-alpine3.14
|
||||||
# DEVELOPMENT
|
|
||||||
##################################
|
|
||||||
FROM node:16-alpine3.14 AS development
|
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
@@ -9,33 +6,10 @@ WORKDIR /usr/src/app
|
|||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
# RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
RUN apk add --update-cache build-base python3
|
||||||
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
#################################
|
|
||||||
# PRODUCTION
|
|
||||||
#################################
|
|
||||||
FROM node:16-alpine3.14 AS production
|
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
ARG NODE_ENV=production
|
|
||||||
ENV NODE_ENV=${NODE_ENV}
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
|
||||||
|
|
||||||
# RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
|
||||||
|
|
||||||
RUN npm install --only=production
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
COPY --from=development /usr/src/app/dist ./dist
|
|
||||||
|
|
||||||
CMD ["node", "dist/main"]
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
# npm run typeorm migration:run
|
# npm run typeorm migration:run
|
||||||
npm run start:dev
|
npm run build && npm run start:prod
|
||||||
126
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "0.0.1",
|
"version": "1.3.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "0.0.1",
|
"version": "1.3.2",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/mapbox-sdk": "^0.13.3",
|
"@mapbox/mapbox-sdk": "^0.13.3",
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"bull": "^4.4.0",
|
"bull": "^4.4.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.13.2",
|
||||||
|
"diskusage": "^1.1.3",
|
||||||
"dotenv": "^14.2.0",
|
"dotenv": "^14.2.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"joi": "^17.5.0",
|
"joi": "^17.5.0",
|
||||||
@@ -1546,6 +1547,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/microservices": {
|
||||||
|
"version": "8.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-8.4.3.tgz",
|
||||||
|
"integrity": "sha512-/ZT5wo1s65J9Cqp2g5eNrYO34VH7/qUkDu4jJyZCT61I9UqpO49J3+1YIAHfmJJzHcrenjgt1sBtlFhwPR3Lgg==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"iterare": "1.2.1",
|
||||||
|
"json-socket": "0.3.0",
|
||||||
|
"tslib": "2.3.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/nest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@grpc/grpc-js": "*",
|
||||||
|
"@nestjs/common": "^8.0.0",
|
||||||
|
"@nestjs/core": "^8.0.0",
|
||||||
|
"@nestjs/websockets": "^8.0.0",
|
||||||
|
"amqp-connection-manager": "*",
|
||||||
|
"amqplib": "*",
|
||||||
|
"cache-manager": "*",
|
||||||
|
"kafkajs": "*",
|
||||||
|
"mqtt": "*",
|
||||||
|
"nats": "*",
|
||||||
|
"redis": "*",
|
||||||
|
"reflect-metadata": "^0.1.12",
|
||||||
|
"rxjs": "^7.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@grpc/grpc-js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@nestjs/websockets": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"amqp-connection-manager": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"amqplib": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"cache-manager": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"kafkajs": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"mqtt": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"nats": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/passport": {
|
"node_modules/@nestjs/passport": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-8.1.0.tgz",
|
||||||
@@ -4258,6 +4319,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/diskusage": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/diskusage/-/diskusage-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-EAyaxl8hy4Ph07kzlzGTfpbZMNAAAHXSZtNEMwdlnSd1noHzvA6HsgKt4fEMSvaEXQYLSphe5rPMxN4WOj0hcQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"es6-promise": "^4.2.5",
|
||||||
|
"nan": "^2.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
@@ -4444,6 +4515,11 @@
|
|||||||
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
|
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/es6-promise": {
|
||||||
|
"version": "4.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||||
|
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||||
@@ -7037,6 +7113,13 @@
|
|||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
||||||
},
|
},
|
||||||
|
"node_modules/json-socket": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-socket/-/json-socket-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-jc8ZbUnYIWdxERFWQKVgwSLkGSe+kyzvmYxwNaRgx/c8NNyuHes4UHnPM3LUrAFXUx1BhNJ94n1h/KCRlbvV0g==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/json-stable-stringify-without-jsonify": {
|
"node_modules/json-stable-stringify-without-jsonify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||||
@@ -7712,8 +7795,7 @@
|
|||||||
"node_modules/nan": {
|
"node_modules/nan": {
|
||||||
"version": "2.15.0",
|
"version": "2.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
|
||||||
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
|
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
@@ -11928,6 +12010,18 @@
|
|||||||
"integrity": "sha512-NFvofzSinp00j5rzUd4tf+xi9od6383iY0JP7o0Bnu1fuItAUkWBgc4EKuIQ3D+c2QI3i9pG1kDWAeY27EMGtg==",
|
"integrity": "sha512-NFvofzSinp00j5rzUd4tf+xi9od6383iY0JP7o0Bnu1fuItAUkWBgc4EKuIQ3D+c2QI3i9pG1kDWAeY27EMGtg==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"@nestjs/microservices": {
|
||||||
|
"version": "8.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-8.4.3.tgz",
|
||||||
|
"integrity": "sha512-/ZT5wo1s65J9Cqp2g5eNrYO34VH7/qUkDu4jJyZCT61I9UqpO49J3+1YIAHfmJJzHcrenjgt1sBtlFhwPR3Lgg==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"iterare": "1.2.1",
|
||||||
|
"json-socket": "0.3.0",
|
||||||
|
"tslib": "2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@nestjs/passport": {
|
"@nestjs/passport": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-8.1.0.tgz",
|
||||||
@@ -14098,6 +14192,15 @@
|
|||||||
"path-type": "^4.0.0"
|
"path-type": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"diskusage": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/diskusage/-/diskusage-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-EAyaxl8hy4Ph07kzlzGTfpbZMNAAAHXSZtNEMwdlnSd1noHzvA6HsgKt4fEMSvaEXQYLSphe5rPMxN4WOj0hcQ==",
|
||||||
|
"requires": {
|
||||||
|
"es6-promise": "^4.2.5",
|
||||||
|
"nan": "^2.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"doctrine": {
|
"doctrine": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
@@ -14246,6 +14349,11 @@
|
|||||||
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
|
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"es6-promise": {
|
||||||
|
"version": "4.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||||
|
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||||
|
},
|
||||||
"escalade": {
|
"escalade": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||||
@@ -16214,6 +16322,13 @@
|
|||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
||||||
},
|
},
|
||||||
|
"json-socket": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-socket/-/json-socket-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-jc8ZbUnYIWdxERFWQKVgwSLkGSe+kyzvmYxwNaRgx/c8NNyuHes4UHnPM3LUrAFXUx1BhNJ94n1h/KCRlbvV0g==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"json-stable-stringify-without-jsonify": {
|
"json-stable-stringify-without-jsonify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||||
@@ -16753,8 +16868,7 @@
|
|||||||
"nan": {
|
"nan": {
|
||||||
"version": "2.15.0",
|
"version": "2.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
|
||||||
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
|
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"natural-compare": {
|
"natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "0.0.1",
|
"version": "1.3.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
"bull": "^4.4.0",
|
"bull": "^4.4.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.13.2",
|
||||||
|
"diskusage": "^1.1.3",
|
||||||
"dotenv": "^14.2.0",
|
"dotenv": "^14.2.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"joi": "^17.5.0",
|
"joi": "^17.5.0",
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ import {
|
|||||||
Response,
|
Response,
|
||||||
Headers,
|
Headers,
|
||||||
Delete,
|
Delete,
|
||||||
|
Logger,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
import { FileFieldsInterceptor, FilesInterceptor } from '@nestjs/platform-express';
|
import { FileFieldsInterceptor } from '@nestjs/platform-express';
|
||||||
import { multerOption } from '../../config/multer-option.config';
|
import { multerOption } from '../../config/multer-option.config';
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { ServeFileDto } from './dto/serve-file.dto';
|
import { ServeFileDto } from './dto/serve-file.dto';
|
||||||
import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service';
|
import { AssetEntity } from './entities/asset.entity';
|
||||||
import { AssetEntity, AssetType } from './entities/asset.entity';
|
|
||||||
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
|
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
|
||||||
import { Response as Res } from 'express';
|
import { Response as Res } from 'express';
|
||||||
import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
|
import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
|
||||||
@@ -55,18 +55,23 @@ export class AssetController {
|
|||||||
@UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] },
|
@UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] },
|
||||||
@Body(ValidationPipe) assetInfo: CreateAssetDto,
|
@Body(ValidationPipe) assetInfo: CreateAssetDto,
|
||||||
) {
|
) {
|
||||||
uploadFiles.assetData.forEach(async (file) => {
|
for (const file of uploadFiles.assetData) {
|
||||||
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
|
try {
|
||||||
|
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
|
||||||
|
|
||||||
if (uploadFiles.thumbnailData != null) {
|
if (uploadFiles.thumbnailData != null && savedAsset) {
|
||||||
await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path);
|
await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path);
|
||||||
await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset);
|
await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset);
|
||||||
|
await this.backgroundTaskService.detectObject(uploadFiles.thumbnailData[0].path, savedAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
|
|
||||||
|
|
||||||
this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset));
|
|
||||||
});
|
|
||||||
|
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
@@ -81,6 +86,11 @@ export class AssetController {
|
|||||||
return this.assetService.serveFile(authUser, query, res, headers);
|
return this.assetService.serveFile(authUser, query, res, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/allObjects')
|
||||||
|
async getCuratedObject(@GetAuthUser() authUser: AuthUserDto) {
|
||||||
|
return this.assetService.getCuratedObject(authUser);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/allLocation')
|
@Get('/allLocation')
|
||||||
async getCuratedLocation(@GetAuthUser() authUser: AuthUserDto) {
|
async getCuratedLocation(@GetAuthUser() authUser: AuthUserDto) {
|
||||||
return this.assetService.getCuratedLocation(authUser);
|
return this.assetService.getCuratedLocation(authUser);
|
||||||
@@ -125,10 +135,10 @@ export class AssetController {
|
|||||||
async deleteAssetById(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) assetIds: DeleteAssetDto) {
|
async deleteAssetById(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) assetIds: DeleteAssetDto) {
|
||||||
const deleteAssetList: AssetEntity[] = [];
|
const deleteAssetList: AssetEntity[] = [];
|
||||||
|
|
||||||
assetIds.ids.forEach(async (id) => {
|
for (const id of assetIds.ids) {
|
||||||
const assets = await this.assetService.getAssetById(authUser, id);
|
const assets = await this.assetService.getAssetById(authUser, id);
|
||||||
deleteAssetList.push(assets);
|
deleteAssetList.push(assets);
|
||||||
});
|
}
|
||||||
|
|
||||||
const result = await this.assetService.deleteAssetById(authUser, assetIds);
|
const result = await this.assetService.deleteAssetById(authUser, assetIds);
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { MoreThan, Repository } from 'typeorm';
|
import { MoreThan, Repository } from 'typeorm';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { UpdateAssetDto } from './dto/update-asset.dto';
|
|
||||||
import { AssetEntity, AssetType } from './entities/asset.entity';
|
import { AssetEntity, AssetType } from './entities/asset.entity';
|
||||||
import _, { result } from 'lodash';
|
import _ from 'lodash';
|
||||||
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
|
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
|
||||||
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
|
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
|
||||||
import { createReadStream, stat } from 'fs';
|
import { createReadStream, stat } from 'fs';
|
||||||
@@ -44,9 +43,7 @@ export class AssetService {
|
|||||||
asset.duration = assetInfo.duration;
|
asset.duration = assetInfo.duration;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await this.assetRepository.save(asset);
|
return await this.assetRepository.save(asset);
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error(`Error Create New Asset ${e}`, 'createUserAsset');
|
Logger.error(`Error Create New Asset ${e}`, 'createUserAsset');
|
||||||
}
|
}
|
||||||
@@ -68,13 +65,11 @@ export class AssetService {
|
|||||||
|
|
||||||
public async getAllAssetsNoPagination(authUser: AuthUserDto) {
|
public async getAllAssetsNoPagination(authUser: AuthUserDto) {
|
||||||
try {
|
try {
|
||||||
const assets = await this.assetRepository
|
return await this.assetRepository
|
||||||
.createQueryBuilder('a')
|
.createQueryBuilder('a')
|
||||||
.where('a."userId" = :userId', { userId: authUser.id })
|
.where('a."userId" = :userId', { userId: authUser.id })
|
||||||
.orderBy('a."createdAt"::date', 'DESC')
|
.orderBy('a."createdAt"::date', 'DESC')
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return assets;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error(e, 'getAllAssets');
|
Logger.error(e, 'getAllAssets');
|
||||||
}
|
}
|
||||||
@@ -226,10 +221,10 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async deleteAssetById(authUser: AuthUserDto, assetIds: DeleteAssetDto) {
|
public async deleteAssetById(authUser: AuthUserDto, assetIds: DeleteAssetDto) {
|
||||||
let result = [];
|
const result = [];
|
||||||
|
|
||||||
const target = assetIds.ids;
|
const target = assetIds.ids;
|
||||||
for (let assetId of target) {
|
for (const assetId of target) {
|
||||||
const res = await this.assetRepository.delete({
|
const res = await this.assetRepository.delete({
|
||||||
id: assetId,
|
id: assetId,
|
||||||
userId: authUser.id,
|
userId: authUser.id,
|
||||||
@@ -251,11 +246,11 @@ export class AssetService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAssetSearchTerm(authUser: AuthUserDto): Promise<String[]> {
|
async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> {
|
||||||
const possibleSearchTerm = new Set<String>();
|
const possibleSearchTerm = new Set<string>();
|
||||||
const rows = await this.assetRepository.query(
|
const rows = await this.assetRepository.query(
|
||||||
`
|
`
|
||||||
select distinct si.tags, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country
|
select distinct si.tags, si.objects, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country
|
||||||
from assets a
|
from assets a
|
||||||
left join exif e on a.id = e."assetId"
|
left join exif e on a.id = e."assetId"
|
||||||
left join smart_info si on a.id = si."assetId"
|
left join smart_info si on a.id = si."assetId"
|
||||||
@@ -268,6 +263,9 @@ export class AssetService {
|
|||||||
// tags
|
// tags
|
||||||
row['tags']?.map((tag) => possibleSearchTerm.add(tag?.toLowerCase()));
|
row['tags']?.map((tag) => possibleSearchTerm.add(tag?.toLowerCase()));
|
||||||
|
|
||||||
|
// objects
|
||||||
|
row['objects']?.map((object) => possibleSearchTerm.add(object?.toLowerCase()));
|
||||||
|
|
||||||
// asset's tyoe
|
// asset's tyoe
|
||||||
possibleSearchTerm.add(row['type']?.toLowerCase());
|
possibleSearchTerm.add(row['type']?.toLowerCase());
|
||||||
|
|
||||||
@@ -301,17 +299,16 @@ export class AssetService {
|
|||||||
AND
|
AND
|
||||||
(
|
(
|
||||||
TO_TSVECTOR('english', ARRAY_TO_STRING(si.tags, ',')) @@ PLAINTO_TSQUERY('english', $2) OR
|
TO_TSVECTOR('english', ARRAY_TO_STRING(si.tags, ',')) @@ PLAINTO_TSQUERY('english', $2) OR
|
||||||
|
TO_TSVECTOR('english', ARRAY_TO_STRING(si.objects, ',')) @@ PLAINTO_TSQUERY('english', $2) OR
|
||||||
e.exif_text_searchable_column @@ PLAINTO_TSQUERY('english', $2)
|
e.exif_text_searchable_column @@ PLAINTO_TSQUERY('english', $2)
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const rows = await this.assetRepository.query(query, [authUser.id, searchAssetDto.searchTerm]);
|
return await this.assetRepository.query(query, [authUser.id, searchAssetDto.searchTerm]);
|
||||||
|
|
||||||
return rows;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCuratedLocation(authUser: AuthUserDto) {
|
async getCuratedLocation(authUser: AuthUserDto) {
|
||||||
const rows = await this.assetRepository.query(
|
return await this.assetRepository.query(
|
||||||
`
|
`
|
||||||
select distinct on (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
|
select distinct on (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
|
||||||
from assets a
|
from assets a
|
||||||
@@ -322,7 +319,18 @@ export class AssetService {
|
|||||||
`,
|
`,
|
||||||
[authUser.id],
|
[authUser.id],
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return rows;
|
async getCuratedObject(authUser: AuthUserDto) {
|
||||||
|
return await this.assetRepository.query(
|
||||||
|
`
|
||||||
|
select distinct on (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
|
||||||
|
from assets a
|
||||||
|
left join smart_info si on a.id = si."assetId"
|
||||||
|
where a."userId" = $1
|
||||||
|
and si.objects is not null
|
||||||
|
`,
|
||||||
|
[authUser.id],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export class SmartInfoEntity {
|
|||||||
@Column({ type: 'text', array: true, nullable: true })
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
|
||||||
|
@Column({ type: 'text', array: true, nullable: true })
|
||||||
|
objects: string[];
|
||||||
|
|
||||||
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
|
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
|
||||||
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
|
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
|
||||||
asset: SmartInfoEntity;
|
asset: SmartInfoEntity;
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
|
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { ServerInfoService } from './server-info.service';
|
import { ServerInfoService } from './server-info.service';
|
||||||
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
|
||||||
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
|
||||||
import { serverVersion } from '../../constants/server_version.constant';
|
import { serverVersion } from '../../constants/server_version.constant';
|
||||||
|
|
||||||
@Controller('server-info')
|
@Controller('server-info')
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import systemInformation from 'systeminformation';
|
|
||||||
import { ServerInfoDto } from './dto/server-info.dto';
|
import { ServerInfoDto } from './dto/server-info.dto';
|
||||||
|
import diskusage from 'diskusage';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerInfoService {
|
export class ServerInfoService {
|
||||||
constructor() {}
|
|
||||||
async getServerInfo() {
|
async getServerInfo() {
|
||||||
const res = await systemInformation.fsSize();
|
const diskInfo = await diskusage.check('./upload');
|
||||||
|
|
||||||
const size = res[0].size;
|
const usagePercentage = (((diskInfo.total - diskInfo.free) / diskInfo.total) * 100).toFixed(2);
|
||||||
const used = res[0].used;
|
|
||||||
const available = res[0].available;
|
|
||||||
const percentageUsage = res[0].use;
|
|
||||||
|
|
||||||
const serverInfo = new ServerInfoDto();
|
const serverInfo = new ServerInfoDto();
|
||||||
serverInfo.diskAvailable = this.getHumanReadableString(available);
|
serverInfo.diskAvailable = ServerInfoService.getHumanReadableString(diskInfo.available);
|
||||||
serverInfo.diskSize = this.getHumanReadableString(size);
|
serverInfo.diskSize = ServerInfoService.getHumanReadableString(diskInfo.total);
|
||||||
serverInfo.diskUse = this.getHumanReadableString(used);
|
serverInfo.diskUse = ServerInfoService.getHumanReadableString(diskInfo.total - diskInfo.free);
|
||||||
serverInfo.diskAvailableRaw = available;
|
serverInfo.diskAvailableRaw = diskInfo.available;
|
||||||
serverInfo.diskSizeRaw = size;
|
serverInfo.diskSizeRaw = diskInfo.total;
|
||||||
serverInfo.diskUseRaw = used;
|
serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
|
||||||
serverInfo.diskUsagePercentage = percentageUsage;
|
serverInfo.diskUsagePercentage = parseFloat(usagePercentage);
|
||||||
|
|
||||||
return serverInfo;
|
return serverInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getHumanReadableString(sizeInByte: number) {
|
private static getHumanReadableString(sizeInByte: number) {
|
||||||
const pepibyte = 1.126 * Math.pow(10, 15);
|
const pepibyte = 1.126 * Math.pow(10, 15);
|
||||||
const tebibyte = 1.1 * Math.pow(10, 12);
|
const tebibyte = 1.1 * Math.pow(10, 12);
|
||||||
const gibibyte = 1.074 * Math.pow(10, 9);
|
const gibibyte = 1.074 * Math.pow(10, 9);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { UserModule } from './api-v1/user/user.module';
|
|||||||
import { AssetModule } from './api-v1/asset/asset.module';
|
import { AssetModule } from './api-v1/asset/asset.module';
|
||||||
import { AuthModule } from './api-v1/auth/auth.module';
|
import { AuthModule } from './api-v1/auth/auth.module';
|
||||||
import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module';
|
import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
|
||||||
import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
|
import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
|
||||||
import { AppLoggerMiddleware } from './middlewares/app-logger.middleware';
|
import { AppLoggerMiddleware } from './middlewares/app-logger.middleware';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
@@ -26,14 +25,12 @@ import { CommunicationModule } from './api-v1/communication/communication.module
|
|||||||
ImmichJwtModule,
|
ImmichJwtModule,
|
||||||
DeviceInfoModule,
|
DeviceInfoModule,
|
||||||
BullModule.forRootAsync({
|
BullModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
useFactory: async () => ({
|
||||||
useFactory: async (configService: ConfigService) => ({
|
|
||||||
redis: {
|
redis: {
|
||||||
host: 'immich_redis',
|
host: 'immich_redis',
|
||||||
port: 6379,
|
port: 6379,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ImageOptimizeModule,
|
ImageOptimizeModule,
|
||||||
@@ -49,6 +46,8 @@ import { CommunicationModule } from './api-v1/communication/communication.module
|
|||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule implements NestModule {
|
||||||
configure(consumer: MiddlewareConsumer): void {
|
configure(consumer: MiddlewareConsumer): void {
|
||||||
// consumer.apply(AppLoggerMiddleware).forRoutes('*');
|
if (process.env.NODE_ENV == 'development') {
|
||||||
|
consumer.apply(AppLoggerMiddleware).forRoutes('*');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
export const serverVersion = {
|
export const serverVersion = {
|
||||||
major: 1,
|
major: 1,
|
||||||
minor: 3,
|
minor: 4,
|
||||||
patch: 0,
|
patch: 0,
|
||||||
build: 0,
|
build: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Logger } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
@@ -10,6 +11,14 @@ async function bootstrap() {
|
|||||||
|
|
||||||
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||||
|
|
||||||
await app.listen(3000);
|
await app.listen(3000, () => {
|
||||||
|
if (process.env.NODE_ENV == 'development') {
|
||||||
|
Logger.log('Running Immich Server in DEVELOPMENT environment', 'IMMICH SERVER');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV == 'production') {
|
||||||
|
Logger.log('Running Immich Server in PRODUCTION environment', 'IMMICH SERVER');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddObjectColumnToSmartInfo1648317474768
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE smart_info
|
||||||
|
ADD COLUMN objects text[];
|
||||||
|
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE smart_info
|
||||||
|
DROP COLUMN objects;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import exifr from 'exifr';
|
import exifr from 'exifr';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import fs, { rmSync } from 'fs';
|
import fs from 'fs';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
|
import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@@ -96,7 +96,7 @@ export class BackgroundTaskProcessor {
|
|||||||
async deleteFileOnDisk(job) {
|
async deleteFileOnDisk(job) {
|
||||||
const { assets }: { assets: AssetEntity[] } = job.data;
|
const { assets }: { assets: AssetEntity[] } = job.data;
|
||||||
|
|
||||||
assets.forEach(async (asset) => {
|
for (const asset of assets) {
|
||||||
fs.unlink(asset.originalPath, (err) => {
|
fs.unlink(asset.originalPath, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log('error deleting ', asset.originalPath);
|
console.log('error deleting ', asset.originalPath);
|
||||||
@@ -108,20 +108,43 @@ export class BackgroundTaskProcessor {
|
|||||||
console.log('error deleting ', asset.originalPath);
|
console.log('error deleting ', asset.originalPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Process('tag-image')
|
@Process('tag-image')
|
||||||
async tagImage(job) {
|
async tagImage(job) {
|
||||||
const { thumbnailPath, asset }: { thumbnailPath: string; asset: AssetEntity } = job.data;
|
const { thumbnailPath, asset }: { thumbnailPath: string; asset: AssetEntity } = job.data;
|
||||||
const res = await axios.post('http://immich_tf_fastapi:8000/tagImage', { thumbnail_path: thumbnailPath });
|
|
||||||
|
|
||||||
if (res.status == 200) {
|
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();
|
const smartInfo = new SmartInfoEntity();
|
||||||
smartInfo.assetId = asset.id;
|
smartInfo.assetId = asset.id;
|
||||||
smartInfo.tags = [...res.data];
|
smartInfo.tags = [...res.data];
|
||||||
|
|
||||||
this.smartInfoRepository.save(smartInfo);
|
await this.smartInfoRepository.upsert(smartInfo, {
|
||||||
|
conflictPaths: ['assetId'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Process('detect-object')
|
||||||
|
async detectObject(job) {
|
||||||
|
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'],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,4 +43,15 @@ export class BackgroundTaskService {
|
|||||||
{ jobId: randomUUID() },
|
{ jobId: randomUUID() },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async detectObject(thumbnailPath: string, asset: AssetEntity) {
|
||||||
|
await this.backgroundTaskQueue.add(
|
||||||
|
'detect-object',
|
||||||
|
{
|
||||||
|
thumbnailPath,
|
||||||
|
asset,
|
||||||
|
},
|
||||||
|
{ jobId: randomUUID() },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||