Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45d4984fde | |||
| ef2f605635 | |||
| 4532db552e | |||
| b9d438006c | |||
| 1eb96fa9e9 | |||
| c2694996e5 | |||
| 4d60133504 | |||
| c0ef300040 | |||
| b05d4fa7d3 | |||
| 84cd91bbbe | |||
| 718c258a07 |
@@ -1,31 +1,30 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
.github/
|
|
||||||
.git/
|
|
||||||
|
|
||||||
design/
|
design/
|
||||||
docker/
|
docker/
|
||||||
docs/
|
docs/
|
||||||
e2e/
|
|
||||||
fastlane/
|
fastlane/
|
||||||
machine-learning/
|
machine-learning/
|
||||||
misc/
|
misc/
|
||||||
mobile/
|
mobile/
|
||||||
|
|
||||||
cli/coverage/
|
|
||||||
cli/dist/
|
|
||||||
cli/node_modules/
|
|
||||||
|
|
||||||
open-api/typescript-sdk/build/
|
|
||||||
open-api/typescript-sdk/node_modules/
|
|
||||||
|
|
||||||
server/coverage/
|
|
||||||
server/node_modules/
|
server/node_modules/
|
||||||
|
server/coverage/
|
||||||
|
server/.reverse-geocoding-dump/
|
||||||
server/upload/
|
server/upload/
|
||||||
server/dist/
|
server/dist/
|
||||||
server/www/
|
|
||||||
server/test/assets/
|
|
||||||
|
|
||||||
web/node_modules/
|
web/node_modules/
|
||||||
web/coverage/
|
web/coverage/
|
||||||
web/.svelte-kit
|
web/.svelte-kit
|
||||||
web/build/
|
web/build/
|
||||||
|
|
||||||
|
cli/node_modules/
|
||||||
|
cli/.reverse-geocoding-dump/
|
||||||
|
cli/upload/
|
||||||
|
cli/dist/
|
||||||
|
|
||||||
|
e2e/
|
||||||
|
|
||||||
|
open-api/typescript-sdk/node_modules/
|
||||||
|
open-api/typescript-sdk/build/
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ max_line_length = off
|
|||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.{yml,yaml}]
|
[*.{yml,yaml}]
|
||||||
quote_type = single
|
quote_type = double
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ mobile/openapi/.openapi-generator/FILES linguist-generated=true
|
|||||||
mobile/lib/**/*.g.dart -diff -merge
|
mobile/lib/**/*.g.dart -diff -merge
|
||||||
mobile/lib/**/*.g.dart linguist-generated=true
|
mobile/lib/**/*.g.dart linguist-generated=true
|
||||||
|
|
||||||
|
open-api/typescript-sdk/axios-client/**/* -diff -merge
|
||||||
|
open-api/typescript-sdk/axios-client/**/* linguist-generated=true
|
||||||
open-api/typescript-sdk/fetch-client.ts -diff -merge
|
open-api/typescript-sdk/fetch-client.ts -diff -merge
|
||||||
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v3.0.0
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.1.0
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
|
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v5.2.0
|
uses: docker/build-push-action@v5.1.0
|
||||||
with:
|
with:
|
||||||
file: cli/Dockerfile
|
file: cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v3.0.0
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.1.0
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
# Workaround to fix error:
|
# Workaround to fix error:
|
||||||
# failed to push: failed to copy: io: read/write on closed pipe
|
# failed to push: failed to copy: io: read/write on closed pipe
|
||||||
# See https://github.com/docker/build-push-action/issues/761
|
# See https://github.com/docker/build-push-action/issues/761
|
||||||
@@ -121,7 +121,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v5.2.0
|
uses: docker/build-push-action@v5.1.0
|
||||||
with:
|
with:
|
||||||
context: ${{ matrix.context }}
|
context: ${{ matrix.context }}
|
||||||
file: ${{ matrix.file }}
|
file: ${{ matrix.file }}
|
||||||
|
|||||||
@@ -10,15 +10,32 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
server-e2e-api:
|
||||||
|
name: Server (e2e-api)
|
||||||
|
runs-on: mich
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./server
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run npm install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run e2e tests
|
||||||
|
run: npm run e2e:api
|
||||||
|
|
||||||
server-e2e-jobs:
|
server-e2e-jobs:
|
||||||
name: Server (e2e-jobs)
|
name: Server (e2e-jobs)
|
||||||
runs-on: ubuntu-latest
|
runs-on: mich
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: "recursive"
|
||||||
|
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
run: make server-e2e-jobs
|
run: make server-e2e-jobs
|
||||||
@@ -167,7 +184,7 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: "recursive"
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -177,44 +194,25 @@ jobs:
|
|||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Run setup cli
|
- name: Run setup cli
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Run linter
|
|
||||||
run: npm run lint
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Run formatter
|
|
||||||
run: npm run format
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Run tsc
|
|
||||||
run: npm run check
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install --with-deps chromium
|
run: npx playwright install --with-deps
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Docker build
|
- name: Docker build
|
||||||
run: docker compose build
|
run: docker compose build
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Run e2e tests (api & cli)
|
- name: Run e2e tests (api & cli)
|
||||||
run: npm run test
|
run: npm run test
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Run e2e tests (web)
|
- name: Run e2e tests (web)
|
||||||
run: npx playwright test
|
run: npx playwright test
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
mobile-unit-tests:
|
mobile-unit-tests:
|
||||||
name: Mobile
|
name: Mobile
|
||||||
@@ -224,8 +222,8 @@ jobs:
|
|||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: "stable"
|
||||||
flutter-version: '3.16.9'
|
flutter-version: "3.16.9"
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: flutter test -j 1
|
run: flutter test -j 1
|
||||||
@@ -243,7 +241,7 @@ jobs:
|
|||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
cache: 'poetry'
|
cache: "poetry"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
poetry install --with dev --with cpu
|
poetry install --with dev --with cpu
|
||||||
@@ -281,7 +279,7 @@ jobs:
|
|||||||
- name: Run API generation
|
- name: Run API generation
|
||||||
run: make open-api
|
run: make open-api
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@v19
|
uses: tj-actions/verify-changed-files@v18
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
@@ -336,7 +334,7 @@ jobs:
|
|||||||
run: npm run typeorm:migrations:generate ./src/infra/migrations/TestMigration
|
run: npm run typeorm:migrations:generate ./src/infra/migrations/TestMigration
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@v19
|
uses: tj-actions/verify-changed-files@v18
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
@@ -354,7 +352,7 @@ jobs:
|
|||||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@v19
|
uses: tj-actions/verify-changed-files@v18
|
||||||
id: verify-changed-sql-files
|
id: verify-changed-sql-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ pull-stage:
|
|||||||
server-e2e-jobs:
|
server-e2e-jobs:
|
||||||
docker compose -f ./server/e2e/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
docker compose -f ./server/e2e/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
||||||
|
|
||||||
|
server-e2e-api:
|
||||||
|
npm run e2e:api --prefix server
|
||||||
|
|
||||||
.PHONY: e2e
|
.PHONY: e2e
|
||||||
e2e:
|
e2e:
|
||||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
|
"@testcontainers/postgresql": "^10.7.1",
|
||||||
"@types/byte-size": "^8.1.0",
|
"@types/byte-size": "^8.1.0",
|
||||||
"@types/cli-progress": "^3.11.0",
|
"@types/cli-progress": "^3.11.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ class Asset {
|
|||||||
assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)),
|
assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)),
|
||||||
deviceAssetId: this.deviceAssetId,
|
deviceAssetId: this.deviceAssetId,
|
||||||
deviceId: 'CLI',
|
deviceId: 'CLI',
|
||||||
fileCreatedAt: this.fileCreatedAt.toISOString(),
|
fileCreatedAt: this.fileCreatedAt,
|
||||||
fileModifiedAt: this.fileModifiedAt.toISOString(),
|
fileModifiedAt: this.fileModifiedAt,
|
||||||
isFavorite: String(false),
|
isFavorite: String(false),
|
||||||
};
|
};
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/p
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import yaml from 'yaml';
|
import yaml from 'yaml';
|
||||||
import { ImmichApi } from './api.service';
|
import { ImmichApi } from './api.service';
|
||||||
|
|
||||||
class LoginError extends Error {
|
class LoginError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
@@ -15,11 +14,13 @@ class LoginError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SessionService {
|
export class SessionService {
|
||||||
private get authPath() {
|
readonly configDirectory!: string;
|
||||||
return path.join(this.configDirectory, '/auth.yml');
|
readonly authPath!: string;
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private configDirectory: string) {}
|
constructor(configDirectory: string) {
|
||||||
|
this.configDirectory = configDirectory;
|
||||||
|
this.authPath = path.join(configDirectory, '/auth.yml');
|
||||||
|
}
|
||||||
|
|
||||||
async connect(): Promise<ImmichApi> {
|
async connect(): Promise<ImmichApi> {
|
||||||
let instanceUrl = process.env.IMMICH_INSTANCE_URL;
|
let instanceUrl = process.env.IMMICH_INSTANCE_URL;
|
||||||
@@ -47,8 +48,6 @@ export class SessionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceUrl = await this.resolveApiEndpoint(instanceUrl);
|
|
||||||
|
|
||||||
const api = new ImmichApi(instanceUrl, apiKey);
|
const api = new ImmichApi(instanceUrl, apiKey);
|
||||||
|
|
||||||
const pingResponse = await api.pingServer().catch((error) => {
|
const pingResponse = await api.pingServer().catch((error) => {
|
||||||
@@ -63,9 +62,7 @@ export class SessionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async login(instanceUrl: string, apiKey: string): Promise<ImmichApi> {
|
async login(instanceUrl: string, apiKey: string): Promise<ImmichApi> {
|
||||||
console.log(`Logging in to ${instanceUrl}`);
|
console.log('Logging in...');
|
||||||
|
|
||||||
instanceUrl = await this.resolveApiEndpoint(instanceUrl);
|
|
||||||
|
|
||||||
const api = new ImmichApi(instanceUrl, apiKey);
|
const api = new ImmichApi(instanceUrl, apiKey);
|
||||||
|
|
||||||
@@ -86,7 +83,7 @@ export class SessionService {
|
|||||||
|
|
||||||
await writeFile(this.authPath, yaml.stringify({ instanceUrl, apiKey }), { mode: 0o600 });
|
await writeFile(this.authPath, yaml.stringify({ instanceUrl, apiKey }), { mode: 0o600 });
|
||||||
|
|
||||||
console.log(`Wrote auth info to ${this.authPath}`);
|
console.log('Wrote auth info to ' + this.authPath);
|
||||||
|
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
@@ -101,18 +98,4 @@ export class SessionService {
|
|||||||
|
|
||||||
console.log('Successfully logged out');
|
console.log('Successfully logged out');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveApiEndpoint(instanceUrl: string): Promise<string> {
|
|
||||||
const wellKnownUrl = new URL('.well-known/immich', instanceUrl);
|
|
||||||
try {
|
|
||||||
const wellKnown = await fetch(wellKnownUrl).then((response) => response.json());
|
|
||||||
const endpoint = new URL(wellKnown.api.endpoint, instanceUrl).toString();
|
|
||||||
if (endpoint !== instanceUrl) {
|
|
||||||
console.debug(`Discovered API at ${endpoint}`);
|
|
||||||
}
|
|
||||||
return endpoint;
|
|
||||||
} catch {
|
|
||||||
return instanceUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# - https://immich.app/docs/developer/setup
|
# - https://immich.app/docs/developer/setup
|
||||||
# - https://immich.app/docs/developer/troubleshooting
|
# - https://immich.app/docs/developer/troubleshooting
|
||||||
|
|
||||||
version: '3.8'
|
version: "3.8"
|
||||||
|
|
||||||
name: immich-dev
|
name: immich-dev
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ x-server-build: &server-common
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: ['/usr/src/app/bin/immich-dev', 'immich']
|
command: [ "/usr/src/app/bin/immich-dev", "immich" ]
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
ports:
|
ports:
|
||||||
- 3001:3001
|
- 3001:3001
|
||||||
@@ -41,7 +41,7 @@ services:
|
|||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
container_name: immich_microservices
|
container_name: immich_microservices
|
||||||
command: ['/usr/src/app/bin/immich-dev', 'microservices']
|
command: [ "/usr/src/app/bin/immich-dev", "microservices" ]
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
@@ -57,7 +57,7 @@ services:
|
|||||||
image: immich-web-dev:latest
|
image: immich-web-dev:latest
|
||||||
build:
|
build:
|
||||||
context: ../web
|
context: ../web
|
||||||
command: ['/usr/src/app/bin/immich-web']
|
command: [ "/usr/src/app/bin/immich-web" ]
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: '3.8'
|
version: "3.8"
|
||||||
|
|
||||||
name: immich-prod
|
name: immich-prod
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ x-server-build: &server-common
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: ['start.sh', 'immich']
|
command: [ "start.sh", "immich" ]
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
ports:
|
ports:
|
||||||
- 2283:3001
|
- 2283:3001
|
||||||
@@ -27,7 +27,7 @@ services:
|
|||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
container_name: immich_microservices
|
container_name: immich_microservices
|
||||||
command: ['start.sh', 'microservices']
|
command: [ "start.sh", "microservices" ]
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: '3.8'
|
version: "3.8"
|
||||||
|
|
||||||
#
|
#
|
||||||
# WARNING: Make sure to use the docker-compose.yml of the current release:
|
# WARNING: Make sure to use the docker-compose.yml of the current release:
|
||||||
@@ -14,7 +14,7 @@ services:
|
|||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||||
command: ['start.sh', 'immich']
|
command: [ "start.sh", "immich" ]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
@@ -33,7 +33,7 @@ services:
|
|||||||
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
|
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||||
command: ['start.sh', 'microservices']
|
command: [ "start.sh", "microservices" ]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
@@ -60,12 +60,12 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
# Configurations for hardware-accelerated transcoding
|
# Configurations for hardware-accelerated transcoding
|
||||||
|
|
||||||
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
||||||
# you can inline the config for a backend by copying its contents
|
# you can inline the config for a backend by copying its contents
|
||||||
# into the immich-microservices service in the docker-compose.yml file.
|
# into the immich-microservices service in the docker-compose.yml file.
|
||||||
|
|
||||||
# See https://immich.app/docs/features/hardware-transcoding for more info on using hardware transcoding.
|
# See https://immich.app/docs/features/hardware-transcoding for more info on using hardware transcoding.
|
||||||
@@ -38,10 +38,12 @@ services:
|
|||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
- /dev/dma_heap:/dev/dma_heap
|
- /dev/dma_heap:/dev/dma_heap
|
||||||
- /dev/mpp_service:/dev/mpp_service
|
- /dev/mpp_service:/dev/mpp_service
|
||||||
#- /dev/mali0:/dev/mali0 # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
|
|
||||||
volumes:
|
volumes:
|
||||||
#- /etc/OpenCL:/etc/OpenCL:ro # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
|
- /usr/bin/ffmpeg:/usr/bin/ffmpeg_mpp:ro
|
||||||
#- /usr/lib/aarch64-linux-gnu/libmali.so.1:/usr/lib/aarch64-linux-gnu/libmali.so.1:ro # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
|
- /lib/aarch64-linux-gnu:/lib/ffmpeg-mpp:ro
|
||||||
|
- /lib/aarch64-linux-gnu/libblas.so.3:/lib/ffmpeg-mpp/libblas.so.3:ro # symlink is resolved by mounting
|
||||||
|
- /lib/aarch64-linux-gnu/liblapack.so.3:/lib/ffmpeg-mpp/liblapack.so.3:ro # symlink is resolved by mounting
|
||||||
|
- /lib/aarch64-linux-gnu/pulseaudio/libpulsecommon-15.99.so:/lib/ffmpeg-mpp/libpulsecommon-15.99.so:ro
|
||||||
|
|
||||||
vaapi:
|
vaapi:
|
||||||
devices:
|
devices:
|
||||||
|
|||||||
@@ -67,11 +67,9 @@ Once you have a new OAuth client application configured, Immich can be configure
|
|||||||
| Client Secret | string | (required) | Required. Client Secret (previous step) |
|
| Client Secret | string | (required) | Required. Client Secret (previous step) |
|
||||||
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||||
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label |
|
|
||||||
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage |
|
|
||||||
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
|
||||||
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
||||||
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
||||||
|
| Storage Claim | string | preferred_username | Claim mapping for the user's storage label |
|
||||||
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
||||||
| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI |
|
| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI |
|
||||||
|
|
||||||
|
|||||||
@@ -42,18 +42,6 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
|
|||||||
- If you have an 11th gen CPU or older, then you may need to follow [these][jellyfin-lp] instructions as Low-Power mode is required
|
- If you have an 11th gen CPU or older, then you may need to follow [these][jellyfin-lp] instructions as Low-Power mode is required
|
||||||
- Additionally, if the server specifically has an 11th gen CPU and is running kernel 5.15 (shipped with Ubuntu 22.04 LTS), then you will need to upgrade this kernel (from [Jellyfin docs][jellyfin-kernel-bug])
|
- Additionally, if the server specifically has an 11th gen CPU and is running kernel 5.15 (shipped with Ubuntu 22.04 LTS), then you will need to upgrade this kernel (from [Jellyfin docs][jellyfin-kernel-bug])
|
||||||
|
|
||||||
#### RKMPP
|
|
||||||
|
|
||||||
For RKMPP to work:
|
|
||||||
|
|
||||||
- You must have a supported Rockchip ARM SoC.
|
|
||||||
- Only RK3588 supports hardware tonemapping, other SoCs use slower software tonemapping while still using hardware encoding.
|
|
||||||
- Tonemapping requires `/usr/lib/aarch64-linux-gnu/libmali.so.1` to be present on your host system. Install [`libmali-valhall-g610-g6p0-gbm`][libmali-rockchip] and modify the [`hwaccel.transcoding.yml`][hw-file] file:
|
|
||||||
- under `rkmpp` uncomment the 3 lines required for OpenCL tonemapping by removing the `#` symbol at the beginning of each line
|
|
||||||
- `- /dev/mali0:/dev/mali0`
|
|
||||||
- `- /etc/OpenCL:/etc/OpenCL:ro`
|
|
||||||
- `- /usr/lib/aarch64-linux-gnu/libmali.so.1:/usr/lib/aarch64-linux-gnu/libmali.so.1:ro`
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
#### Basic Setup
|
#### Basic Setup
|
||||||
@@ -118,4 +106,3 @@ Once this is done, you can continue to step 3 of "Basic Setup".
|
|||||||
[nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/
|
[nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/
|
||||||
[jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux
|
[jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux
|
||||||
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
|
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
|
||||||
[libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Note: Either a manual or scheduled library scan must have been performed to iden
|
|||||||
|
|
||||||
In all above scan methods, Immich will check if any files are missing. This can happen if files are deleted, or if they are on a storage location that is currently unavailable, like a network drive that is not mounted, or a USB drive that has been unplugged. In order to prevent accidental deletion of assets, Immich will not immediately delete an asset from the library if the file is missing. Instead, the asset will be internally marked as offline and will still be visible in the main timeline. If the file is moved back to its original location and the library is scanned again, the asset will be restored.
|
In all above scan methods, Immich will check if any files are missing. This can happen if files are deleted, or if they are on a storage location that is currently unavailable, like a network drive that is not mounted, or a USB drive that has been unplugged. In order to prevent accidental deletion of assets, Immich will not immediately delete an asset from the library if the file is missing. Instead, the asset will be internally marked as offline and will still be visible in the main timeline. If the file is moved back to its original location and the library is scanned again, the asset will be restored.
|
||||||
|
|
||||||
Finally, files can be deleted from Immich via the `Remove Offline Files` job. This job can be found by the three dots menu for the associated external storage that was configured under Administration > Libraries (the same location described at [create external libraries](#create-external-libraries)). When this job is run, any assets marked as offline will then be removed from Immich. Run this job whenever files have been deleted from the file system and you want to remove them from Immich.
|
Finally, files can be deleted from Immich via the `Remove Offline Files` job. This job can be found by the three dots menu for the associated external storage that was configured under user account settings > libraries (the same location described at [create external libraries](#create-external-libraries)). When this job is run, any assets marked as offline will then be removed from Immich. Run this job whenever files have been deleted from the file system and you want to remove them from Immich.
|
||||||
|
|
||||||
### Import Paths
|
### Import Paths
|
||||||
|
|
||||||
@@ -50,6 +50,8 @@ If the import paths are edited in a way that an external file is no longer in an
|
|||||||
|
|
||||||
Sometimes, an external library will not scan correctly. This can happen if immich_server or immich_microservices can't access the files. Here are some things to check:
|
Sometimes, an external library will not scan correctly. This can happen if immich_server or immich_microservices can't access the files. Here are some things to check:
|
||||||
|
|
||||||
|
- Is the external path set correctly? Each import path must be contained in the external path.
|
||||||
|
- Make sure the external path does not contain spaces
|
||||||
- In the docker-compose file, are the volumes mounted correctly?
|
- In the docker-compose file, are the volumes mounted correctly?
|
||||||
- Are the volumes identical between the `server` and `microservices` container?
|
- Are the volumes identical between the `server` and `microservices` container?
|
||||||
- Are the import paths set correctly, and do they match the path set in docker-compose file?
|
- Are the import paths set correctly, and do they match the path set in docker-compose file?
|
||||||
@@ -59,6 +61,18 @@ Sometimes, an external library will not scan correctly. This can happen if immic
|
|||||||
|
|
||||||
To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_microservices /bin/bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the `immich_server` container. If you cannot access this directory in both the `microservices` and `server` containers, Immich won't be able to import files.
|
To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_microservices /bin/bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the `immich_server` container. If you cannot access this directory in both the `microservices` and `server` containers, Immich won't be able to import files.
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
|
||||||
|
Please read and understand this section before setting external paths, as there are important security considerations.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
For security purposes, each Immich user is disallowed to add external files by default. This is to prevent devastating [path traversal attacks](https://owasp.org/www-community/attacks/Path_Traversal). An admin can allow individual users to use external path feature via the `external path` setting found in the admin panel. Without the external path restriction, a user can add any image or video file on the Immich host filesystem to be imported into Immich, potentially allowing sensitive data to be accessed. If you are running Immich as root in your Docker setup (which is the default), all external file reads are done with root privileges. This is particularly dangerous if the Immich host is a shared server.
|
||||||
|
|
||||||
|
With the `external path` set, a user is restricted to accessing external files to files or directories within that path. The Immich admin should still be careful not set the external path too generously. For example, `user1` wants to read their photos in to `/home/user1`. A lazy admin sets that user's external path to `/home/` since it "gets the job done". However, that user will then be able to read all photos in `/home/user2/private-photos`, too! Please set the external path as specific as possible. If multiple folders must be added, do this using the docker volume mount feature described below.
|
||||||
|
|
||||||
### Exclusion Patterns
|
### Exclusion Patterns
|
||||||
|
|
||||||
By default, all files in the import paths will be added to the library. If there are files that should not be added, exclusion patterns can be used to exclude them. Exclusion patterns are glob patterns are matched against the full file path. If a file matches an exclusion pattern, it will not be added to the library. Exclusion patterns can be added in the Scan Settings page for each library. Under the hood, Immich uses the [glob](https://www.npmjs.com/package/glob) package to match patterns, so please refer to [their documentation](https://github.com/isaacs/node-glob#glob-primer) to see what patterns are supported.
|
By default, all files in the import paths will be added to the library. If there are files that should not be added, exclusion patterns can be used to exclude them. Exclusion patterns are glob patterns are matched against the full file path. If a file matches an exclusion pattern, it will not be added to the library. Exclusion patterns can be added in the Scan Settings page for each library. Under the hood, Immich uses the [glob](https://www.npmjs.com/package/glob) package to match patterns, so please refer to [their documentation](https://github.com/isaacs/node-glob#glob-primer) to see what patterns are supported.
|
||||||
@@ -74,17 +88,10 @@ Some basic examples:
|
|||||||
|
|
||||||
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button.
|
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button.
|
||||||
|
|
||||||
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
|
If your photos are on a network drive you will likely have to enable filesystem polling. The performance hit for polling large libraries is currently unknown, feel free to test this feature and report back. In addition to the boolean feature flag, the configuration file allows customization of the following parameters, please see the [chokidar documentation](https://github.com/paulmillr/chokidar?tab=readme-ov-file#performance) for reference.
|
||||||
|
|
||||||
#### Troubleshooting
|
- `usePolling` (default: `false`).
|
||||||
|
- `interval`. (default: 10000). When using polling, this is how often (in milliseconds) the filesystem is polled.
|
||||||
If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watched` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files.
|
|
||||||
|
|
||||||
```
|
|
||||||
ERROR [LibraryService] Library watcher for library c69faf55-f96d-4aa0-b83b-2d80cbc27d98 encountered error: Error: ENOSPC: System limit for number of file watchers reached, watch '/media/photo.jpg'
|
|
||||||
```
|
|
||||||
|
|
||||||
In rare cases, the library watcher can hang, preventing Immich from starting up. In this case, disable the library watcher in the configuration file. If the watcher is enabled from within Immich, the app must be started without the microservices. Disable the microservices in the docker compose file, start Immich, disable the library watcher in the admin settings, close Immich, re-enable the microservices, and then Immich can be started normally.
|
|
||||||
|
|
||||||
### Nightly job
|
### Nightly job
|
||||||
|
|
||||||
@@ -131,13 +138,27 @@ The `ro` flag at the end only gives read-only access to the volumes. While Immic
|
|||||||
_Remember to bring the container `docker compose down/up` to register the changes. Make sure you can see the mounted path in the container._
|
_Remember to bring the container `docker compose down/up` to register the changes. Make sure you can see the mounted path in the container._
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
### Set External Path
|
||||||
|
|
||||||
|
Only an admin can do this.
|
||||||
|
|
||||||
|
- Navigate to `Administration > Users` page on the web.
|
||||||
|
- Click on the user edit button.
|
||||||
|
- Set `/mnt/media` to be the external path. This folder will only contain the three folders that we want to import, so nothing else can be accessed.
|
||||||
|
:::note
|
||||||
|
Spaces in the internal path aren't currently supported.
|
||||||
|
|
||||||
|
You must import it as:
|
||||||
|
`..:/mnt/media/my-media:ro`
|
||||||
|
instead of
|
||||||
|
`..:/mnt/media/my media:ro`
|
||||||
|
:::
|
||||||
|
|
||||||
### Create External Libraries
|
### Create External Libraries
|
||||||
|
|
||||||
These actions must be performed by the Immich administrator.
|
- Click on your user name in the top right corner -> Account Settings
|
||||||
|
- Click on Libraries
|
||||||
- Click on Administration -> Libraries
|
|
||||||
- Click on Create External Library
|
- Click on Create External Library
|
||||||
- Select which user owns the library, this can not be changed later
|
|
||||||
- Click the drop-down menu on the newly created library
|
- Click the drop-down menu on the newly created library
|
||||||
- Click on Rename Library and rename it to "Christmas Trip"
|
- Click on Rename Library and rename it to "Christmas Trip"
|
||||||
- Click Edit Import Paths
|
- Click Edit Import Paths
|
||||||
@@ -148,7 +169,7 @@ NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/
|
|||||||
|
|
||||||
Next, we'll add an exclusion pattern to filter out raw files.
|
Next, we'll add an exclusion pattern to filter out raw files.
|
||||||
|
|
||||||
- Click the drop-down menu on the newly-created Christmas library
|
- Click the drop-down menu on the newly christmas library
|
||||||
- Click on Manage
|
- Click on Manage
|
||||||
- Click on Scan Settings
|
- Click on Scan Settings
|
||||||
- Click on Add Exclusion Pattern
|
- Click on Add Exclusion Pattern
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Run `docker exec -it immich_postgres psql immich <DB_USERNAME>` to connect to th
|
|||||||
## Assets
|
## Assets
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
The `"originalFileName"` column is the name of the file at time of upload, including the extension.
|
The `"originalFileName"` column is the name of the uploaded file _without_ the extension.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
```sql title="Find by original filename"
|
```sql title="Find by original filename"
|
||||||
@@ -40,10 +40,6 @@ SELECT * FROM "assets" where "livePhotoVideoId" IS NOT NULL;
|
|||||||
SELECT "assets".* FROM "exif" LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId" WHERE "exif"."assetId" IS NULL;
|
SELECT "assets".* FROM "exif" LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId" WHERE "exif"."assetId" IS NULL;
|
||||||
```
|
```
|
||||||
|
|
||||||
```sql title="size < 100,000 bytes, smallest to largest"
|
|
||||||
SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "exif"."fileSizeInByte" < 100000 ORDER BY "exif"."fileSizeInByte" ASC;
|
|
||||||
```
|
|
||||||
|
|
||||||
```sql title="Without thumbnails"
|
```sql title="Without thumbnails"
|
||||||
SELECT * FROM "assets" WHERE "assets"."resizePath" IS NULL OR "assets"."webpPath" IS NULL;
|
SELECT * FROM "assets" WHERE "assets"."resizePath" IS NULL OR "assets"."webpPath" IS NULL;
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ On my computer, for example, I use this path:
|
|||||||
EXTERNAL_PATH=/home/tenino/photos
|
EXTERNAL_PATH=/home/tenino/photos
|
||||||
```
|
```
|
||||||
|
|
||||||
:::info EXTERNAL_PATH design
|
|
||||||
The design choice to put the EXTERNAL_PATH into .env rather than put two copies of the absolute path in the yml file in order to make everything easier, so if you have two copies of the same path that have to be kept in sync, then someday later when you move the data, update only one of the paths, without everything will break mysteriously.
|
|
||||||
:::
|
|
||||||
|
|
||||||
Restart Immich.
|
Restart Immich.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -39,26 +35,47 @@ docker compose down
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
# Create the library
|
# Set the External Path
|
||||||
|
|
||||||
In the Immich web UI:
|
In the Immich web UI:
|
||||||
|
|
||||||
- click the **Administration** link in the upper right corner.
|
- click the **Administration** link in the upper right corner.
|
||||||
<img src={require('./img/administration-link.png').default} width="50%" title="Administration link" />
|
<img src={require('./img/administration-link.png').default} width="50%" title="Administration link" />
|
||||||
|
|
||||||
- Select the **External Libraries** tab
|
- Select the **Users** tab
|
||||||
<img src={require('./img/external-libraries.png').default} width="50%" title="External Libraries tab" />
|
<img src={require('./img/users-tab.png').default} width="50%" title="Users tab" />
|
||||||
|
|
||||||
- Click the **Create Library** button
|
- Select the **pencil** next to your user ID
|
||||||
<img src={require('./img/create-external-library.png').default} width="50%" title="Create Library button" />
|
<img src={require('./img/pencil.png').default} width="50%" title="Pencil" />
|
||||||
|
|
||||||
- In the dialog, select which user should own the new library
|
- Fill in the **External Path** field with `/usr/src/app/external`
|
||||||
<img src={require('./img/library-owner.png').default} width="50%" title="Library owner diaglog" />
|
<img src={require('./img/external-path.png').default} width="50%" title="External Path field" />
|
||||||
|
|
||||||
|
Notice this matches the path _inside the container_ where we mounted your photos.
|
||||||
|
The purpose of the external path field is for administrators who have multiple users
|
||||||
|
on their Immich instance. It lets you prevent other authorized users from
|
||||||
|
navigating to your external library.
|
||||||
|
|
||||||
|
# Import the library
|
||||||
|
|
||||||
|
In the Immich web UI:
|
||||||
|
|
||||||
|
- Click your user avatar in the upper-right corner (circle with your initials)
|
||||||
|
<img src={require('./img/user-avatar.png').default} width="50%" title="User avatar" />
|
||||||
|
|
||||||
|
- Click **Account Settings**
|
||||||
|
<img src={require('./img/account-settings.png').default} width="50%" title="Account Settings button" />
|
||||||
|
|
||||||
|
- Click to expand **Libraries**
|
||||||
|
<img src={require('./img/libraries-dropdown.png').default} width="50%" title="Libraries dropdown" />
|
||||||
|
|
||||||
|
- Click the **Create External Library** button
|
||||||
|
<img src={require('./img/create-external-library-button.png').default} width="50%" title="Create External Library button" />
|
||||||
|
|
||||||
- Click the three-dots menu and select **Edit Import Paths**
|
- Click the three-dots menu and select **Edit Import Paths**
|
||||||
<img src={require('./img/edit-import-paths.png').default} width="50%" title="Edit Import Paths menu option" />
|
<img src={require('./img/edit-import-paths.png').default} width="50%" title="Edit Import Paths menu option" />
|
||||||
|
|
||||||
- Click Add path
|
- Click \*_Add path_
|
||||||
<img src={require('./img/add-path-button.png').default} width="50%" title="Add Path button" />
|
<img src={require('./img/add-path-button.png').default} width="50%" title="Add Path button" />
|
||||||
|
|
||||||
- Enter **/usr/src/app/external** as the path and click Add
|
- Enter **/usr/src/app/external** as the path and click Add
|
||||||
|
|||||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -95,16 +95,13 @@ The default configuration looks like this:
|
|||||||
"issuerUrl": "",
|
"issuerUrl": "",
|
||||||
"clientId": "",
|
"clientId": "",
|
||||||
"clientSecret": "",
|
"clientSecret": "",
|
||||||
|
"mobileOverrideEnabled": false,
|
||||||
|
"mobileRedirectUri": "",
|
||||||
"scope": "openid email profile",
|
"scope": "openid email profile",
|
||||||
"signingAlgorithm": "RS256",
|
|
||||||
"storageLabelClaim": "preferred_username",
|
"storageLabelClaim": "preferred_username",
|
||||||
"storageQuotaClaim": "immich_quota",
|
|
||||||
"defaultStorageQuota": 0,
|
|
||||||
"buttonText": "Login with OAuth",
|
"buttonText": "Login with OAuth",
|
||||||
"autoRegister": true,
|
"autoRegister": true,
|
||||||
"autoLaunch": false,
|
"autoLaunch": false
|
||||||
"mobileOverrideEnabled": false,
|
|
||||||
"mobileRedirectUri": ""
|
|
||||||
},
|
},
|
||||||
"passwordLogin": {
|
"passwordLogin": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
@@ -128,9 +125,6 @@ The default configuration looks like this:
|
|||||||
"theme": {
|
"theme": {
|
||||||
"customCss": ""
|
"customCss": ""
|
||||||
},
|
},
|
||||||
"user": {
|
|
||||||
"deleteDelay": 7
|
|
||||||
},
|
|
||||||
"library": {
|
"library": {
|
||||||
"scan": {
|
"scan": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
|||||||
| `DB_PORT` | Database Port | `5432` | server, microservices |
|
| `DB_PORT` | Database Port | `5432` | server, microservices |
|
||||||
| `DB_USERNAME` | Database User | `postgres` | server, microservices |
|
| `DB_USERNAME` | Database User | `postgres` | server, microservices |
|
||||||
| `DB_PASSWORD` | Database Password | `postgres` | server, microservices |
|
| `DB_PASSWORD` | Database Password | `postgres` | server, microservices |
|
||||||
| `DB_DATABASE_NAME` | Database Name | `immich` | server, microservices |
|
| `DB_DATABASE` | Database Name | `immich` | server, microservices |
|
||||||
| `DB_VECTOR_EXTENSION`<sup>\*1</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server, microservices |
|
| `DB_VECTOR_EXTENSION`<sup>\*1</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server, microservices |
|
||||||
|
|
||||||
\*1: This setting cannot be changed after the server has successfully started up
|
\*1: This setting cannot be changed after the server has successfully started up
|
||||||
@@ -124,18 +124,16 @@ Redis (Sentinel) URL example JSON before encoding:
|
|||||||
|
|
||||||
## Machine Learning
|
## Machine Learning
|
||||||
|
|
||||||
| Variable | Description | Default | Services |
|
| Variable | Description | Default | Services |
|
||||||
| :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------: | :--------------- |
|
| :----------------------------------------------- | :----------------------------------------------------------------- | :-----------------: | :--------------- |
|
||||||
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
|
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
|
||||||
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
|
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
|
||||||
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
|
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
|
||||||
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
|
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
|
||||||
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
|
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
|
||||||
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
|
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
|
||||||
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||||
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning |
|
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning |
|
||||||
| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
|
|
||||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
|
|
||||||
|
|
||||||
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
|
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ Or before beginning app installation, [create the datasets](https://www.truenas.
|
|||||||
Immich requires seven datasets: **library**, **pgBackup**, **pgData**, **profile**, **thumbs**, **uploads**, and **video**.
|
Immich requires seven datasets: **library**, **pgBackup**, **pgData**, **profile**, **thumbs**, **uploads**, and **video**.
|
||||||
You can organize these as one parent with seven child datasets, for example `mnt/tank/immich/library`, `mnt/tank/immich/pgBackup`, and so on.
|
You can organize these as one parent with seven child datasets, for example `mnt/tank/immich/library`, `mnt/tank/immich/pgBackup`, and so on.
|
||||||
|
|
||||||
:::info Permissions
|
|
||||||
The **pgData** dataset must be owned by the user `netdata` (UID 999) for postgres to start. The other datasets must be owned by the user `root` (UID 0) or a group that includes the user `root` (UID 0) for immich to have the necessary permissions.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Installing the Immich Application
|
## Installing the Immich Application
|
||||||
|
|
||||||
To install the **Immich** application, go to **Apps**, click **Discover Apps**, either begin typing Immich into the search field or scroll down to locate the **Immich** application widget.
|
To install the **Immich** application, go to **Apps**, click **Discover Apps**, either begin typing Immich into the search field or scroll down to locate the **Immich** application widget.
|
||||||
|
|||||||
@@ -12231,7 +12231,7 @@
|
|||||||
"mime-format": "2.0.0",
|
"mime-format": "2.0.0",
|
||||||
"mime-types": "2.1.27",
|
"mime-types": "2.1.27",
|
||||||
"postman-url-encoder": "2.1.3",
|
"postman-url-encoder": "2.1.3",
|
||||||
"sanitize-html": "^2.12.1",
|
"sanitize-html": "^2.11.0",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
"uuid": "3.4.0"
|
"uuid": "3.4.0"
|
||||||
}
|
}
|
||||||
@@ -14762,9 +14762,9 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
},
|
},
|
||||||
"node_modules/sanitize-html": {
|
"node_modules/sanitize-html": {
|
||||||
"version": "2.12.1",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz",
|
||||||
"integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==",
|
"integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
|||||||
@@ -50,22 +50,12 @@ import {
|
|||||||
mdiVectorCombine,
|
mdiVectorCombine,
|
||||||
mdiVideo,
|
mdiVideo,
|
||||||
mdiWeb,
|
mdiWeb,
|
||||||
mdiScaleBalance,
|
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Timeline, { DateType, Item } from '../components/timeline';
|
import Timeline, { DateType, Item } from '../components/timeline';
|
||||||
|
|
||||||
const items: Item[] = [
|
const items: Item[] = [
|
||||||
{
|
|
||||||
icon: mdiScaleBalance,
|
|
||||||
description: 'Immich switches to AGPLv3 license',
|
|
||||||
title: 'AGPL License',
|
|
||||||
release: 'v1.95.0',
|
|
||||||
tag: 'v1.95.0',
|
|
||||||
date: new Date(2024, 1, 20),
|
|
||||||
dateType: DateType.RELEASE,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: mdiEyeRefreshOutline,
|
icon: mdiEyeRefreshOutline,
|
||||||
description: 'Automatically import files in external libraries when the operating system detects changes.',
|
description: 'Automatically import files in external libraries when the operating system detects changes.',
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: 'tsconfig.json',
|
|
||||||
sourceType: 'module',
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
|
||||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
|
||||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: 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',
|
|
||||||
'@typescript-eslint/no-floating-promises': 'error',
|
|
||||||
'unicorn/prefer-module': 'off',
|
|
||||||
curly: 2,
|
|
||||||
'prettier/prettier': 0,
|
|
||||||
'unicorn/prevent-abbreviations': 'off',
|
|
||||||
'unicorn/filename-case': 'off',
|
|
||||||
'unicorn/no-null': 'off',
|
|
||||||
'unicorn/prefer-top-level-await': 'off',
|
|
||||||
'unicorn/prefer-event-target': 'off',
|
|
||||||
'unicorn/no-thenable': 'off',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
/build
|
|
||||||
/package
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
*.md
|
|
||||||
*.json
|
|
||||||
coverage
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"printWidth": 120,
|
|
||||||
"semi": true,
|
|
||||||
"organizeImportsSkipDestructiveCodeActions": true,
|
|
||||||
"plugins": ["prettier-plugin-organize-imports"]
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
version: '3.8'
|
version: "3.8"
|
||||||
|
|
||||||
name: immich-e2e
|
name: immich-e2e
|
||||||
|
|
||||||
x-server-build: &server-common
|
x-server-build: &server-common
|
||||||
image: immich-server:latest
|
image: immich-server:latest
|
||||||
|
container_name: immich-e2e-server
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
@@ -16,23 +17,20 @@ x-server-build: &server-common
|
|||||||
- IMMICH_MACHINE_LEARNING_ENABLED=false
|
- IMMICH_MACHINE_LEARNING_ENABLED=false
|
||||||
volumes:
|
volumes:
|
||||||
- upload:/usr/src/app/upload
|
- upload:/usr/src/app/upload
|
||||||
- ../server/test/assets:/data/assets
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich-e2e-server
|
command: [ "./start.sh", "immich" ]
|
||||||
command: ['./start.sh', 'immich']
|
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
ports:
|
ports:
|
||||||
- 2283:3001
|
- 2283:3001
|
||||||
|
|
||||||
immich-microservices:
|
# immich-microservices:
|
||||||
container_name: immich-e2e-microservices
|
# command: [ "./start.sh", "microservices" ]
|
||||||
command: ['./start.sh', 'microservices']
|
# <<: *server-common
|
||||||
<<: *server-common
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.98.2",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest --run",
|
"test": "vitest --config vitest.config.ts",
|
||||||
"test:watch": "vitest",
|
|
||||||
"test:web": "npx playwright test",
|
"test:web": "npx playwright test",
|
||||||
"start:web": "npx playwright test --ui",
|
"start:web": "npx playwright test --ui"
|
||||||
"format": "prettier --check .",
|
|
||||||
"format:fix": "prettier --write .",
|
|
||||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
|
||||||
"lint:fix": "npm run lint -- --fix",
|
|
||||||
"check": "tsc --noEmit"
|
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -22,25 +16,11 @@
|
|||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.41.2",
|
"@playwright/test": "^1.41.2",
|
||||||
"@types/luxon": "^3.4.2",
|
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.11.17",
|
||||||
"@types/pg": "^8.11.0",
|
"@types/pg": "^8.11.0",
|
||||||
"@types/pngjs": "^6.0.4",
|
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
|
||||||
"@typescript-eslint/parser": "^7.1.0",
|
|
||||||
"@vitest/coverage-v8": "^1.3.0",
|
"@vitest/coverage-v8": "^1.3.0",
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
|
||||||
"eslint-plugin-unicorn": "^51.0.1",
|
|
||||||
"exiftool-vendored": "^24.5.0",
|
|
||||||
"luxon": "^3.4.4",
|
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"pngjs": "^7.0.0",
|
|
||||||
"prettier": "^3.2.5",
|
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
|
||||||
"socket.io-client": "^4.7.4",
|
|
||||||
"supertest": "^6.3.4",
|
"supertest": "^6.3.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vitest": "^1.3.0"
|
"vitest": "^1.3.0"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ActivityCreateDto,
|
ActivityCreateDto,
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AssetFileUploadResponseDto,
|
AssetResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
ReactionType,
|
ReactionType,
|
||||||
createActivity as create,
|
createActivity as create,
|
||||||
@@ -9,25 +9,29 @@ import {
|
|||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('/activity', () => {
|
describe('/activity', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let nonOwner: LoginResponseDto;
|
let nonOwner: LoginResponseDto;
|
||||||
let asset: AssetFileUploadResponseDto;
|
let asset: AssetResponseDto;
|
||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
|
|
||||||
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
|
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
|
||||||
create({ activityCreateDto: dto }, { headers: asBearerAuth(accessToken || admin.accessToken) });
|
create(
|
||||||
|
{ activityCreateDto: dto },
|
||||||
|
{ headers: asBearerAuth(accessToken || admin.accessToken) }
|
||||||
|
);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
|
await dbUtils.reset();
|
||||||
|
|
||||||
admin = await utils.adminSetup();
|
admin = await apiUtils.adminSetup();
|
||||||
nonOwner = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
nonOwner = await apiUtils.userSetup(admin.accessToken, createUserDto.user1);
|
||||||
asset = await utils.createAsset(admin.accessToken);
|
asset = await apiUtils.createAsset(admin.accessToken);
|
||||||
album = await createAlbum(
|
album = await createAlbum(
|
||||||
{
|
{
|
||||||
createAlbumDto: {
|
createAlbumDto: {
|
||||||
@@ -36,12 +40,12 @@ describe('/activity', () => {
|
|||||||
sharedWithUserIds: [nonOwner.userId],
|
sharedWithUserIds: [nonOwner.userId],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase(['activity']);
|
await dbUtils.reset(['activity']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /activity', () => {
|
describe('GET /activity', () => {
|
||||||
@@ -52,9 +56,13 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require an albumId', async () => {
|
it('should require an albumId', async () => {
|
||||||
const { status, body } = await request(app).get('/activity').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app)
|
||||||
|
.get('/activity')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid albumId', async () => {
|
it('should reject an invalid albumId', async () => {
|
||||||
@@ -63,7 +71,9 @@ describe('/activity', () => {
|
|||||||
.query({ albumId: uuidDto.invalid })
|
.query({ albumId: uuidDto.invalid })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid assetId', async () => {
|
it('should reject an invalid assetId', async () => {
|
||||||
@@ -72,7 +82,9 @@ describe('/activity', () => {
|
|||||||
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
|
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID']))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should start off empty', async () => {
|
it('should start off empty', async () => {
|
||||||
@@ -92,7 +104,7 @@ describe('/activity', () => {
|
|||||||
assetIds: [asset.id],
|
assetIds: [asset.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
);
|
);
|
||||||
|
|
||||||
const [reaction] = await Promise.all([
|
const [reaction] = await Promise.all([
|
||||||
@@ -148,7 +160,9 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should filter by userId', async () => {
|
it('should filter by userId', async () => {
|
||||||
const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
|
const [reaction] = await Promise.all([
|
||||||
|
createActivity({ albumId: album.id, type: ReactionType.Like }),
|
||||||
|
]);
|
||||||
|
|
||||||
const response1 = await request(app)
|
const response1 = await request(app)
|
||||||
.get('/activity')
|
.get('/activity')
|
||||||
@@ -201,7 +215,9 @@ describe('/activity', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ albumId: uuidDto.invalid });
|
.send({ albumId: uuidDto.invalid });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a comment when type is comment', async () => {
|
it('should require a comment when type is comment', async () => {
|
||||||
@@ -210,7 +226,12 @@ describe('/activity', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
|
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['comment must be a string', 'comment should not be empty']));
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest([
|
||||||
|
'comment must be a string',
|
||||||
|
'comment should not be empty',
|
||||||
|
])
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add a comment to an album', async () => {
|
it('should add a comment to an album', async () => {
|
||||||
@@ -250,7 +271,9 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return a 200 for a duplicate like on the album', async () => {
|
it('should return a 200 for a duplicate like on the album', async () => {
|
||||||
const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
|
const [reaction] = await Promise.all([
|
||||||
|
createActivity({ albumId: album.id, type: ReactionType.Like }),
|
||||||
|
]);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/activity')
|
.post('/activity')
|
||||||
@@ -333,7 +356,9 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
describe('DELETE /activity/:id', () => {
|
describe('DELETE /activity/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(`/activity/${uuidDto.notFound}`);
|
const { status, body } = await request(app).delete(
|
||||||
|
`/activity/${uuidDto.notFound}`
|
||||||
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -395,7 +420,9 @@ describe('/activity', () => {
|
|||||||
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
|
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest('Not found or no activity.delete access'));
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest('Not found or no activity.delete access')
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should let a non-owner remove their own comment', async () => {
|
it('should let a non-owner remove their own comment', async () => {
|
||||||
@@ -405,7 +432,7 @@ describe('/activity', () => {
|
|||||||
type: ReactionType.Comment,
|
type: ReactionType.Comment,
|
||||||
comment: 'This is a test comment',
|
comment: 'This is a test comment',
|
||||||
},
|
},
|
||||||
nonOwner.accessToken,
|
nonOwner.accessToken
|
||||||
);
|
);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AssetFileUploadResponseDto,
|
AssetResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -21,56 +21,57 @@ const user2NotShared = 'user2NotShared';
|
|||||||
describe('/album', () => {
|
describe('/album', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user1Asset1: AssetFileUploadResponseDto;
|
let user1Asset1: AssetResponseDto;
|
||||||
let user1Asset2: AssetFileUploadResponseDto;
|
let user1Asset2: AssetResponseDto;
|
||||||
let user1Albums: AlbumResponseDto[];
|
let user1Albums: AlbumResponseDto[];
|
||||||
let user2: LoginResponseDto;
|
let user2: LoginResponseDto;
|
||||||
let user2Albums: AlbumResponseDto[];
|
let user2Albums: AlbumResponseDto[];
|
||||||
let user3: LoginResponseDto; // deleted
|
let user3: LoginResponseDto; // deleted
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
|
await dbUtils.reset();
|
||||||
|
|
||||||
admin = await utils.adminSetup();
|
admin = await apiUtils.adminSetup();
|
||||||
|
|
||||||
[user1, user2, user3] = await Promise.all([
|
[user1, user2, user3] = await Promise.all([
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user1),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user2),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user3),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
[user1Asset1, user1Asset2] = await Promise.all([
|
[user1Asset1, user1Asset2] = await Promise.all([
|
||||||
utils.createAsset(user1.accessToken, { isFavorite: true }),
|
apiUtils.createAsset(user1.accessToken),
|
||||||
utils.createAsset(user1.accessToken),
|
apiUtils.createAsset(user1.accessToken),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const albums = await Promise.all([
|
const albums = await Promise.all([
|
||||||
// user 1
|
// user 1
|
||||||
utils.createAlbum(user1.accessToken, {
|
apiUtils.createAlbum(user1.accessToken, {
|
||||||
albumName: user1SharedUser,
|
albumName: user1SharedUser,
|
||||||
sharedWithUserIds: [user2.userId],
|
sharedWithUserIds: [user2.userId],
|
||||||
assetIds: [user1Asset1.id],
|
assetIds: [user1Asset1.id],
|
||||||
}),
|
}),
|
||||||
utils.createAlbum(user1.accessToken, {
|
apiUtils.createAlbum(user1.accessToken, {
|
||||||
albumName: user1SharedLink,
|
albumName: user1SharedLink,
|
||||||
assetIds: [user1Asset1.id],
|
assetIds: [user1Asset1.id],
|
||||||
}),
|
}),
|
||||||
utils.createAlbum(user1.accessToken, {
|
apiUtils.createAlbum(user1.accessToken, {
|
||||||
albumName: user1NotShared,
|
albumName: user1NotShared,
|
||||||
assetIds: [user1Asset1.id, user1Asset2.id],
|
assetIds: [user1Asset1.id, user1Asset2.id],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// user 2
|
// user 2
|
||||||
utils.createAlbum(user2.accessToken, {
|
apiUtils.createAlbum(user2.accessToken, {
|
||||||
albumName: user2SharedUser,
|
albumName: user2SharedUser,
|
||||||
sharedWithUserIds: [user1.userId],
|
sharedWithUserIds: [user1.userId],
|
||||||
assetIds: [user1Asset1.id],
|
assetIds: [user1Asset1.id],
|
||||||
}),
|
}),
|
||||||
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
apiUtils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
||||||
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
apiUtils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
||||||
|
|
||||||
// user 3
|
// user 3
|
||||||
utils.createAlbum(user3.accessToken, {
|
apiUtils.createAlbum(user3.accessToken, {
|
||||||
albumName: 'Deleted',
|
albumName: 'Deleted',
|
||||||
sharedWithUserIds: [user1.userId],
|
sharedWithUserIds: [user1.userId],
|
||||||
}),
|
}),
|
||||||
@@ -81,18 +82,21 @@ describe('/album', () => {
|
|||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// add shared link to user1SharedLink album
|
// add shared link to user1SharedLink album
|
||||||
utils.createSharedLink(user1.accessToken, {
|
apiUtils.createSharedLink(user1.accessToken, {
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
albumId: user1Albums[1].id,
|
albumId: user1Albums[1].id,
|
||||||
}),
|
}),
|
||||||
// add shared link to user2SharedLink album
|
// add shared link to user2SharedLink album
|
||||||
utils.createSharedLink(user2.accessToken, {
|
apiUtils.createSharedLink(user2.accessToken, {
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
albumId: user2Albums[1].id,
|
albumId: user2Albums[1].id,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await deleteUser({ id: user3.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
await deleteUser(
|
||||||
|
{ id: user3.userId },
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /album', () => {
|
describe('GET /album', () => {
|
||||||
@@ -107,7 +111,9 @@ describe('/album', () => {
|
|||||||
.get('/album?shared=invalid')
|
.get('/album?shared=invalid')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest(['shared must be a boolean value'])
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid assetId param', async () => {
|
it('should reject an invalid assetId param', async () => {
|
||||||
@@ -118,17 +124,6 @@ describe('/album', () => {
|
|||||||
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
|
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not show other users' favorites", async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/album/${user1Albums[0].id}?withoutAssets=false`)
|
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`);
|
|
||||||
expect(status).toEqual(200);
|
|
||||||
expect(body).toEqual({
|
|
||||||
...user1Albums[0],
|
|
||||||
assets: [expect.objectContaining({ isFavorite: false })],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not return shared albums with a deleted owner', async () => {
|
it('should not return shared albums with a deleted owner', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/album?shared=true')
|
.get('/album?shared=true')
|
||||||
@@ -153,12 +148,14 @@ describe('/album', () => {
|
|||||||
albumName: user2SharedUser,
|
albumName: user2SharedUser,
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the album collection including owned and shared', async () => {
|
it('should return the album collection including owned and shared', async () => {
|
||||||
const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`);
|
const { status, body } = await request(app)
|
||||||
|
.get('/album')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toHaveLength(3);
|
expect(body).toHaveLength(3);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
@@ -178,7 +175,7 @@ describe('/album', () => {
|
|||||||
albumName: user1NotShared,
|
albumName: user1NotShared,
|
||||||
shared: false,
|
shared: false,
|
||||||
}),
|
}),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,7 +202,7 @@ describe('/album', () => {
|
|||||||
albumName: user2SharedUser,
|
albumName: user2SharedUser,
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -222,7 +219,7 @@ describe('/album', () => {
|
|||||||
albumName: user1NotShared,
|
albumName: user1NotShared,
|
||||||
shared: false,
|
shared: false,
|
||||||
}),
|
}),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -253,7 +250,9 @@ describe('/album', () => {
|
|||||||
|
|
||||||
describe('GET /album/:id', () => {
|
describe('GET /album/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(`/album/${user1Albums[0].id}`);
|
const { status, body } = await request(app).get(
|
||||||
|
`/album/${user1Albums[0].id}`
|
||||||
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -266,7 +265,7 @@ describe('/album', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
...user1Albums[0],
|
...user1Albums[0],
|
||||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
assets: [expect.objectContaining(user1Albums[0].assets[0])],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -278,7 +277,7 @@ describe('/album', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
...user2Albums[0],
|
...user2Albums[0],
|
||||||
assets: [expect.objectContaining({ id: user2Albums[0].assets[0].id })],
|
assets: [expect.objectContaining(user2Albums[0].assets[0])],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -290,7 +289,7 @@ describe('/album', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
...user1Albums[0],
|
...user1Albums[0],
|
||||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
assets: [expect.objectContaining(user1Albums[0].assets[0])],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -327,7 +326,9 @@ describe('/album', () => {
|
|||||||
|
|
||||||
describe('POST /album', () => {
|
describe('POST /album', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post('/album').send({ albumName: 'New album' });
|
const { status, body } = await request(app)
|
||||||
|
.post('/album')
|
||||||
|
.send({ albumName: 'New album' });
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -359,31 +360,37 @@ describe('/album', () => {
|
|||||||
|
|
||||||
describe('PUT /album/:id/assets', () => {
|
describe('PUT /album/:id/assets', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/assets`);
|
const { status, body } = await request(app).put(
|
||||||
|
`/album/${user1Albums[0].id}/assets`
|
||||||
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to add own asset to own album', async () => {
|
it('should be able to add own asset to own album', async () => {
|
||||||
const asset = await utils.createAsset(user1.accessToken);
|
const asset = await apiUtils.createAsset(user1.accessToken);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/album/${user1Albums[0].id}/assets`)
|
.put(`/album/${user1Albums[0].id}/assets`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ ids: [asset.id] });
|
.send({ ids: [asset.id] });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
|
expect(body).toEqual([
|
||||||
|
expect.objectContaining({ id: asset.id, success: true }),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to add own asset to shared album', async () => {
|
it('should be able to add own asset to shared album', async () => {
|
||||||
const asset = await utils.createAsset(user1.accessToken);
|
const asset = await apiUtils.createAsset(user1.accessToken);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/album/${user2Albums[0].id}/assets`)
|
.put(`/album/${user2Albums[0].id}/assets`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ ids: [asset.id] });
|
.send({ ids: [asset.id] });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
|
expect(body).toEqual([
|
||||||
|
expect.objectContaining({ id: asset.id, success: true }),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -397,7 +404,7 @@ describe('/album', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update an album', async () => {
|
it('should update an album', async () => {
|
||||||
const album = await utils.createAlbum(user1.accessToken, {
|
const album = await apiUtils.createAlbum(user1.accessToken, {
|
||||||
albumName: 'New album',
|
albumName: 'New album',
|
||||||
});
|
});
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -466,7 +473,9 @@ describe('/album', () => {
|
|||||||
.send({ ids: [user1Asset1.id] });
|
.send({ ids: [user1Asset1.id] });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
|
expect(body).toEqual([
|
||||||
|
expect.objectContaining({ id: user1Asset1.id, success: true }),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to remove own asset from shared album', async () => {
|
it('should be able to remove own asset from shared album', async () => {
|
||||||
@@ -476,7 +485,9 @@ describe('/album', () => {
|
|||||||
.send({ ids: [user1Asset1.id] });
|
.send({ ids: [user1Asset1.id] });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
|
expect(body).toEqual([
|
||||||
|
expect.objectContaining({ id: user1Asset1.id, success: true }),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -484,13 +495,15 @@ describe('/album', () => {
|
|||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
album = await utils.createAlbum(user1.accessToken, {
|
album = await apiUtils.createAlbum(user1.accessToken, {
|
||||||
albumName: 'testAlbum',
|
albumName: 'testAlbum',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
|
const { status, body } = await request(app)
|
||||||
|
.put(`/album/${user1Albums[0].id}/users`)
|
||||||
|
.send({ sharedUserIds: [] });
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -506,7 +519,7 @@ describe('/album', () => {
|
|||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
sharedUsers: [expect.objectContaining({ id: user2.userId })],
|
sharedUsers: [expect.objectContaining({ id: user2.userId })],
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,42 @@
|
|||||||
import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk';
|
import {
|
||||||
import { asBearerAuth, utils } from 'src/utils';
|
deleteAssets,
|
||||||
|
getAuditFiles,
|
||||||
|
updateAsset,
|
||||||
|
type LoginResponseDto,
|
||||||
|
} from '@immich/sdk';
|
||||||
|
import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('/audit', () => {
|
describe('/audit', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
await utils.resetFilesystem();
|
await dbUtils.reset();
|
||||||
|
await fileUtils.reset();
|
||||||
|
|
||||||
admin = await utils.adminSetup();
|
admin = await apiUtils.adminSetup();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET :/file-report', () => {
|
describe('GET :/file-report', () => {
|
||||||
it('excludes assets without issues from report', async () => {
|
it('excludes assets without issues from report', async () => {
|
||||||
const [trashedAsset, archivedAsset] = await Promise.all([
|
const [trashedAsset, archivedAsset, _] = await Promise.all([
|
||||||
utils.createAsset(admin.accessToken),
|
apiUtils.createAsset(admin.accessToken),
|
||||||
utils.createAsset(admin.accessToken),
|
apiUtils.createAsset(admin.accessToken),
|
||||||
utils.createAsset(admin.accessToken),
|
apiUtils.createAsset(admin.accessToken),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
deleteAssets({ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { headers: asBearerAuth(admin.accessToken) }),
|
deleteAssets(
|
||||||
|
{ assetBulkDeleteDto: { ids: [trashedAsset.id] } },
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
|
),
|
||||||
updateAsset(
|
updateAsset(
|
||||||
{
|
{
|
||||||
id: archivedAsset.id,
|
id: archivedAsset.id,
|
||||||
updateAssetDto: { isArchived: true },
|
updateAssetDto: { isArchived: true },
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk';
|
import {
|
||||||
|
LoginResponseDto,
|
||||||
|
getAuthDevices,
|
||||||
|
login,
|
||||||
|
signUpAdmin,
|
||||||
|
} from '@immich/sdk';
|
||||||
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
|
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
|
||||||
import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
|
import {
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
deviceDto,
|
||||||
|
errorDto,
|
||||||
|
loginResponseDto,
|
||||||
|
signupResponseDto,
|
||||||
|
} from 'src/responses';
|
||||||
|
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const { name, email, password } = signupDto.admin;
|
const { name, email, password } = signupDto.admin;
|
||||||
|
|
||||||
describe(`/auth/admin-sign-up`, () => {
|
describe(`/auth/admin-sign-up`, () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
apiUtils.setup();
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase();
|
await dbUtils.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /auth/admin-sign-up', () => {
|
describe('POST /auth/admin-sign-up', () => {
|
||||||
@@ -34,14 +48,18 @@ describe(`/auth/admin-sign-up`, () => {
|
|||||||
|
|
||||||
for (const { should, data } of invalid) {
|
for (const { should, data } of invalid) {
|
||||||
it(`should ${should}`, async () => {
|
it(`should ${should}`, async () => {
|
||||||
const { status, body } = await request(app).post('/auth/admin-sign-up').send(data);
|
const { status, body } = await request(app)
|
||||||
|
.post('/auth/admin-sign-up')
|
||||||
|
.send(data);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it(`should sign up the admin`, async () => {
|
it(`should sign up the admin`, async () => {
|
||||||
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
|
const { status, body } = await request(app)
|
||||||
|
.post('/auth/admin-sign-up')
|
||||||
|
.send(signupDto.admin);
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
expect(body).toEqual(signupResponseDto.admin);
|
expect(body).toEqual(signupResponseDto.admin);
|
||||||
});
|
});
|
||||||
@@ -68,7 +86,9 @@ describe(`/auth/admin-sign-up`, () => {
|
|||||||
it('should not allow a second admin to sign up', async () => {
|
it('should not allow a second admin to sign up', async () => {
|
||||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||||
|
|
||||||
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
|
const { status, body } = await request(app)
|
||||||
|
.post('/auth/admin-sign-up')
|
||||||
|
.send(signupDto.admin);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.alreadyHasAdmin);
|
expect(body).toEqual(errorDto.alreadyHasAdmin);
|
||||||
@@ -80,14 +100,16 @@ describe('/auth/*', () => {
|
|||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase();
|
await dbUtils.reset();
|
||||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||||
admin = await login({ loginCredentialDto: loginDto.admin });
|
admin = await login({ loginCredentialDto: loginDto.admin });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`POST /auth/login`, () => {
|
describe(`POST /auth/login`, () => {
|
||||||
it('should reject an incorrect password', async () => {
|
it('should reject an incorrect password', async () => {
|
||||||
const { status, body } = await request(app).post('/auth/login').send({ email, password: 'incorrect' });
|
const { status, body } = await request(app)
|
||||||
|
.post('/auth/login')
|
||||||
|
.send({ email, password: 'incorrect' });
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.incorrectLogin);
|
expect(body).toEqual(errorDto.incorrectLogin);
|
||||||
});
|
});
|
||||||
@@ -103,7 +125,9 @@ describe('/auth/*', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('should accept a correct password', async () => {
|
it('should accept a correct password', async () => {
|
||||||
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
|
const { status, body, headers } = await request(app)
|
||||||
|
.post('/auth/login')
|
||||||
|
.send({ email, password });
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
expect(body).toEqual(loginResponseDto.admin);
|
expect(body).toEqual(loginResponseDto.admin);
|
||||||
|
|
||||||
@@ -112,9 +136,15 @@ describe('/auth/*', () => {
|
|||||||
|
|
||||||
const cookies = headers['set-cookie'];
|
const cookies = headers['set-cookie'];
|
||||||
expect(cookies).toHaveLength(3);
|
expect(cookies).toHaveLength(3);
|
||||||
expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`);
|
expect(cookies[0]).toEqual(
|
||||||
expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;');
|
`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`
|
||||||
expect(cookies[2]).toEqual('immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;');
|
);
|
||||||
|
expect(cookies[1]).toEqual(
|
||||||
|
'immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;'
|
||||||
|
);
|
||||||
|
expect(cookies[2]).toEqual(
|
||||||
|
'immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,12 +176,18 @@ describe('/auth/*', () => {
|
|||||||
await login({ loginCredentialDto: loginDto.admin });
|
await login({ loginCredentialDto: loginDto.admin });
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
|
await expect(
|
||||||
|
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
|
||||||
|
).resolves.toHaveLength(6);
|
||||||
|
|
||||||
const { status } = await request(app).delete(`/auth/devices`).set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status } = await request(app)
|
||||||
|
.delete(`/auth/devices`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
|
|
||||||
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
|
await expect(
|
||||||
|
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
|
||||||
|
).resolves.toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error for a non-existent device id', async () => {
|
it('should throw an error for a non-existent device id', async () => {
|
||||||
@@ -159,7 +195,9 @@ describe('/auth/*', () => {
|
|||||||
.delete(`/auth/devices/${uuidDto.notFound}`)
|
.delete(`/auth/devices/${uuidDto.notFound}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest('Not found or no authDevice.delete access')
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should logout a device', async () => {
|
it('should logout a device', async () => {
|
||||||
@@ -181,7 +219,9 @@ describe('/auth/*', () => {
|
|||||||
|
|
||||||
describe('POST /auth/validateToken', () => {
|
describe('POST /auth/validateToken', () => {
|
||||||
it('should reject an invalid token', async () => {
|
it('should reject an invalid token', async () => {
|
||||||
const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
|
const { status, body } = await request(app)
|
||||||
|
.post(`/auth/validateToken`)
|
||||||
|
.set('Authorization', 'Bearer 123');
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.invalidToken);
|
expect(body).toEqual(errorDto.invalidToken);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
|
import { AssetResponseDto, LoginResponseDto } from '@immich/sdk';
|
||||||
import { readFile, writeFile } from 'node:fs/promises';
|
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, tempDir, utils } from 'src/utils';
|
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('/download', () => {
|
describe('/download', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let asset1: AssetFileUploadResponseDto;
|
let asset1: AssetResponseDto;
|
||||||
let asset2: AssetFileUploadResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
admin = await utils.adminSetup();
|
await dbUtils.reset();
|
||||||
[asset1, asset2] = await Promise.all([utils.createAsset(admin.accessToken), utils.createAsset(admin.accessToken)]);
|
admin = await apiUtils.adminSetup();
|
||||||
|
asset1 = await apiUtils.createAsset(admin.accessToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /download/info', () => {
|
describe('POST /download/info', () => {
|
||||||
@@ -36,47 +35,16 @@ describe('/download', () => {
|
|||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
archives: [expect.objectContaining({ assetIds: [asset1.id] })],
|
archives: [expect.objectContaining({ assetIds: [asset1.id] })],
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /download/archive', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post(`/download/archive`)
|
|
||||||
.send({ assetIds: [asset1.id, asset2.id] });
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should download an archive', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/download/archive')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ assetIds: [asset1.id, asset2.id] });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body instanceof Buffer).toBe(true);
|
|
||||||
|
|
||||||
await writeFile(`${tempDir}/archive.zip`, body);
|
|
||||||
await utils.unzip(`${tempDir}/archive.zip`, `${tempDir}/archive`);
|
|
||||||
const files = [
|
|
||||||
{ filename: 'example.png', id: asset1.id },
|
|
||||||
{ filename: 'example+1.png', id: asset2.id },
|
|
||||||
];
|
|
||||||
for (const { id, filename } of files) {
|
|
||||||
const bytes = await readFile(`${tempDir}/archive/${filename}`);
|
|
||||||
const asset = await utils.getAssetInfo(admin.accessToken, id);
|
|
||||||
expect(utils.sha1(bytes)).toBe(asset.checksum);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /download/asset/:id', () => {
|
describe('POST /download/asset/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(`/download/asset/${asset1.id}`);
|
const { status, body } = await request(app).post(
|
||||||
|
`/download/asset/${asset1.id}`
|
||||||
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -88,7 +56,7 @@ describe('/download', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.headers['content-type']).toEqual('image/png');
|
expect(response.headers['content-type']).toEqual('image/jpeg');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,455 +0,0 @@
|
|||||||
import { LibraryResponseDto, LibraryType, LoginResponseDto, getAllLibraries } from '@immich/sdk';
|
|
||||||
import { userDto, uuidDto } from 'src/fixtures';
|
|
||||||
import { errorDto } from 'src/responses';
|
|
||||||
import { app, asBearerAuth, testAssetDirInternal, utils } from 'src/utils';
|
|
||||||
import request from 'supertest';
|
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
describe('/library', () => {
|
|
||||||
let admin: LoginResponseDto;
|
|
||||||
let user: LoginResponseDto;
|
|
||||||
let library: LibraryResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await utils.resetDatabase();
|
|
||||||
admin = await utils.adminSetup();
|
|
||||||
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
|
||||||
library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /library', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/library');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should start with a default upload library', async () => {
|
|
||||||
const { status, body } = await request(app).get('/library').set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
ownerId: admin.userId,
|
|
||||||
type: LibraryType.Upload,
|
|
||||||
name: 'Default Library',
|
|
||||||
refreshedAt: null,
|
|
||||||
assetCount: 0,
|
|
||||||
importPaths: [],
|
|
||||||
exclusionPatterns: [],
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /library', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post('/library').send({});
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require admin authentication', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
|
||||||
.send({ type: LibraryType.External });
|
|
||||||
|
|
||||||
expect(status).toBe(403);
|
|
||||||
expect(body).toEqual(errorDto.forbidden);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create an external library with defaults', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ type: LibraryType.External });
|
|
||||||
|
|
||||||
expect(status).toBe(201);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
ownerId: admin.userId,
|
|
||||||
type: LibraryType.External,
|
|
||||||
name: 'New External Library',
|
|
||||||
refreshedAt: null,
|
|
||||||
assetCount: 0,
|
|
||||||
importPaths: [],
|
|
||||||
exclusionPatterns: [],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create an external library with options', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({
|
|
||||||
type: LibraryType.External,
|
|
||||||
name: 'My Awesome Library',
|
|
||||||
importPaths: ['/path/to/import'],
|
|
||||||
exclusionPatterns: ['**/Raw/**'],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(status).toBe(201);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
name: 'My Awesome Library',
|
|
||||||
importPaths: ['/path/to/import'],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not create an external library with duplicate import paths', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({
|
|
||||||
type: LibraryType.External,
|
|
||||||
name: 'My Awesome Library',
|
|
||||||
importPaths: ['/path', '/path'],
|
|
||||||
exclusionPatterns: ['**/Raw/**'],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"]));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not create an external library with duplicate exclusion patterns', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({
|
|
||||||
type: LibraryType.External,
|
|
||||||
name: 'My Awesome Library',
|
|
||||||
importPaths: ['/path/to/import'],
|
|
||||||
exclusionPatterns: ['**/Raw/**', '**/Raw/**'],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create an upload library with defaults', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ type: LibraryType.Upload });
|
|
||||||
|
|
||||||
expect(status).toBe(201);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
ownerId: admin.userId,
|
|
||||||
type: LibraryType.Upload,
|
|
||||||
name: 'New Upload Library',
|
|
||||||
refreshedAt: null,
|
|
||||||
assetCount: 0,
|
|
||||||
importPaths: [],
|
|
||||||
exclusionPatterns: [],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create an upload library with options', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ type: LibraryType.Upload, name: 'My Awesome Library' });
|
|
||||||
|
|
||||||
expect(status).toBe(201);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
name: 'My Awesome Library',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow upload libraries to have import paths', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ type: LibraryType.Upload, importPaths: ['/path/to/import'] });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have import paths'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow upload libraries to have exclusion patterns', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have exclusion patterns'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('PUT /library/:id', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).put(`/library/${uuidDto.notFound}`).send({});
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should change the library name', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ name: 'New Library Name' });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
name: 'New Library Name',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set an empty name', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ name: '' });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(['name should not be empty']));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should change the import paths', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ importPaths: [testAssetDirInternal] });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
importPaths: [testAssetDirInternal],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject an empty import path', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ importPaths: [''] });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(['each value in importPaths should not be empty']));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject duplicate import paths', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ importPaths: ['/path', '/path'] });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"]));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should change the exclusion pattern', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ exclusionPatterns: ['**/Raw/**'] });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
exclusionPatterns: ['**/Raw/**'],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject duplicate exclusion patterns', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject an empty exclusion pattern', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ exclusionPatterns: [''] });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(['each value in exclusionPatterns should not be empty']));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /library/:id', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}`);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require admin access', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/library/${uuidDto.notFound}`)
|
|
||||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
|
||||||
expect(status).toBe(403);
|
|
||||||
expect(body).toEqual(errorDto.forbidden);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get library by id', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
ownerId: admin.userId,
|
|
||||||
type: LibraryType.External,
|
|
||||||
name: 'New External Library',
|
|
||||||
refreshedAt: null,
|
|
||||||
assetCount: 0,
|
|
||||||
importPaths: [],
|
|
||||||
exclusionPatterns: [],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('DELETE /library/:id', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not delete the last upload library', async () => {
|
|
||||||
const libraries = await getAllLibraries(
|
|
||||||
{ $type: LibraryType.Upload },
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
|
||||||
);
|
|
||||||
|
|
||||||
const adminLibraries = libraries.filter((library) => library.ownerId === admin.userId);
|
|
||||||
expect(adminLibraries.length).toBeGreaterThanOrEqual(1);
|
|
||||||
const lastLibrary = adminLibraries.pop() as LibraryResponseDto;
|
|
||||||
|
|
||||||
// delete all but the last upload library
|
|
||||||
for (const library of adminLibraries) {
|
|
||||||
const { status } = await request(app)
|
|
||||||
.delete(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(204);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.delete(`/library/${lastLibrary.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(body).toEqual(errorDto.noDeleteUploadLibrary);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete an external library', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.delete(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(204);
|
|
||||||
expect(body).toEqual({});
|
|
||||||
|
|
||||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
expect(libraries).not.toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: library.id,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /library/:id/statistics', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}/statistics`);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /library/:id/scan', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/scan`).send({});
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /library/:id/removeOffline', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/removeOffline`).send({});
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /library/:id/validate', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/validate`).send({});
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass with no import paths', async () => {
|
|
||||||
const response = await utils.validateLibrary(admin.accessToken, library.id, { importPaths: [] });
|
|
||||||
expect(response.importPaths).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail if path does not exist', async () => {
|
|
||||||
const pathToTest = `${testAssetDirInternal}/does/not/exist`;
|
|
||||||
|
|
||||||
const response = await utils.validateLibrary(admin.accessToken, library.id, {
|
|
||||||
importPaths: [pathToTest],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.importPaths?.length).toEqual(1);
|
|
||||||
const pathResponse = response?.importPaths?.at(0);
|
|
||||||
|
|
||||||
expect(pathResponse).toEqual({
|
|
||||||
importPath: pathToTest,
|
|
||||||
isValid: false,
|
|
||||||
message: `Path does not exist (ENOENT)`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail if path is a file', async () => {
|
|
||||||
const pathToTest = `${testAssetDirInternal}/albums/nature/el_torcal_rocks.jpg`;
|
|
||||||
|
|
||||||
const response = await utils.validateLibrary(admin.accessToken, library.id, {
|
|
||||||
importPaths: [pathToTest],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.importPaths?.length).toEqual(1);
|
|
||||||
const pathResponse = response?.importPaths?.at(0);
|
|
||||||
|
|
||||||
expect(pathResponse).toEqual({
|
|
||||||
importPath: pathToTest,
|
|
||||||
isValid: false,
|
|
||||||
message: `Not a directory`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +1,30 @@
|
|||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, utils } from 'src/utils';
|
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe(`/oauth`, () => {
|
describe(`/oauth`, () => {
|
||||||
beforeAll(async () => {
|
beforeAll(() => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
await utils.adminSetup();
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await dbUtils.reset();
|
||||||
|
await apiUtils.adminSetup();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /oauth/authorize', () => {
|
describe('POST /oauth/authorize', () => {
|
||||||
it(`should throw an error if a redirect uri is not provided`, async () => {
|
it(`should throw an error if a redirect uri is not provided`, async () => {
|
||||||
const { status, body } = await request(app).post('/oauth/authorize').send({});
|
const { status, body } = await request(app)
|
||||||
|
.post('/oauth/authorize')
|
||||||
|
.send({});
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty']));
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest([
|
||||||
|
'redirectUri must be a string',
|
||||||
|
'redirectUri should not be empty',
|
||||||
|
])
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LoginResponseDto, createPartner } from '@immich/sdk';
|
import { LoginResponseDto, createPartner } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -12,19 +12,26 @@ describe('/partner', () => {
|
|||||||
let user3: LoginResponseDto;
|
let user3: LoginResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
|
await dbUtils.reset();
|
||||||
|
|
||||||
admin = await utils.adminSetup();
|
admin = await apiUtils.adminSetup();
|
||||||
|
|
||||||
[user1, user2, user3] = await Promise.all([
|
[user1, user2, user3] = await Promise.all([
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user1),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user2),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user3),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
createPartner({ id: user2.userId }, { headers: asBearerAuth(user1.accessToken) }),
|
createPartner(
|
||||||
createPartner({ id: user1.userId }, { headers: asBearerAuth(user2.accessToken) }),
|
{ id: user2.userId },
|
||||||
|
{ headers: asBearerAuth(user1.accessToken) }
|
||||||
|
),
|
||||||
|
createPartner(
|
||||||
|
{ id: user1.userId },
|
||||||
|
{ headers: asBearerAuth(user2.accessToken) }
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,7 +66,9 @@ describe('/partner', () => {
|
|||||||
|
|
||||||
describe('POST /partner/:id', () => {
|
describe('POST /partner/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(`/partner/${user3.userId}`);
|
const { status, body } = await request(app).post(
|
||||||
|
`/partner/${user3.userId}`
|
||||||
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -80,13 +89,17 @@ describe('/partner', () => {
|
|||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(expect.objectContaining({ message: 'Partner already exists' }));
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({ message: 'Partner already exists' })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /partner/:id', () => {
|
describe('PUT /partner/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(`/partner/${user2.userId}`);
|
const { status, body } = await request(app).put(
|
||||||
|
`/partner/${user2.userId}`
|
||||||
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -99,13 +112,17 @@ describe('/partner', () => {
|
|||||||
.send({ inTimeline: false });
|
.send({ inTimeline: false });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual(expect.objectContaining({ id: user2.userId, inTimeline: false }));
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({ id: user2.userId, inTimeline: false })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /partner/:id', () => {
|
describe('DELETE /partner/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(`/partner/${user3.userId}`);
|
const { status, body } = await request(app).delete(
|
||||||
|
`/partner/${user3.userId}`
|
||||||
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -125,7 +142,9 @@ describe('/partner', () => {
|
|||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(expect.objectContaining({ message: 'Partner not found' }));
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({ message: 'Partner not found' })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,49 +1,39 @@
|
|||||||
import { LoginResponseDto, PersonResponseDto } from '@immich/sdk';
|
import { LoginResponseDto, PersonResponseDto } from '@immich/sdk';
|
||||||
import { uuidDto } from 'src/fixtures';
|
import { uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, utils } from 'src/utils';
|
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const invalidBirthday = [
|
describe('/activity', () => {
|
||||||
{ birthDate: 'false', response: 'birthDate must be a date string' },
|
|
||||||
{ birthDate: '123567', response: 'birthDate must be a date string' },
|
|
||||||
{ birthDate: 123_567, response: 'birthDate must be a date string' },
|
|
||||||
{ birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] },
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('/person', () => {
|
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let visiblePerson: PersonResponseDto;
|
let visiblePerson: PersonResponseDto;
|
||||||
let hiddenPerson: PersonResponseDto;
|
let hiddenPerson: PersonResponseDto;
|
||||||
let multipleAssetsPerson: PersonResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
admin = await utils.adminSetup();
|
await dbUtils.reset();
|
||||||
|
admin = await apiUtils.adminSetup();
|
||||||
|
});
|
||||||
|
|
||||||
[visiblePerson, hiddenPerson, multipleAssetsPerson] = await Promise.all([
|
beforeEach(async () => {
|
||||||
utils.createPerson(admin.accessToken, {
|
await dbUtils.reset(['person']);
|
||||||
|
|
||||||
|
[visiblePerson, hiddenPerson] = await Promise.all([
|
||||||
|
apiUtils.createPerson(admin.accessToken, {
|
||||||
name: 'visible_person',
|
name: 'visible_person',
|
||||||
}),
|
}),
|
||||||
utils.createPerson(admin.accessToken, {
|
apiUtils.createPerson(admin.accessToken, {
|
||||||
name: 'hidden_person',
|
name: 'hidden_person',
|
||||||
isHidden: true,
|
isHidden: true,
|
||||||
}),
|
}),
|
||||||
utils.createPerson(admin.accessToken, {
|
|
||||||
name: 'multiple_assets_person',
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const asset1 = await utils.createAsset(admin.accessToken);
|
const asset = await apiUtils.createAsset(admin.accessToken);
|
||||||
const asset2 = await utils.createAsset(admin.accessToken);
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }),
|
dbUtils.createFace({ assetId: asset.id, personId: visiblePerson.id }),
|
||||||
utils.createFace({ assetId: asset1.id, personId: hiddenPerson.id }),
|
dbUtils.createFace({ assetId: asset.id, personId: hiddenPerson.id }),
|
||||||
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
|
|
||||||
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
|
|
||||||
utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }),
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,10 +55,9 @@ describe('/person', () => {
|
|||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
total: 3,
|
total: 2,
|
||||||
hidden: 1,
|
hidden: 1,
|
||||||
people: [
|
people: [
|
||||||
expect.objectContaining({ name: 'multiple_assets_person' }),
|
|
||||||
expect.objectContaining({ name: 'visible_person' }),
|
expect.objectContaining({ name: 'visible_person' }),
|
||||||
expect.objectContaining({ name: 'hidden_person' }),
|
expect.objectContaining({ name: 'hidden_person' }),
|
||||||
],
|
],
|
||||||
@@ -76,23 +65,24 @@ describe('/person', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return only visible people', async () => {
|
it('should return only visible people', async () => {
|
||||||
const { status, body } = await request(app).get('/person').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app)
|
||||||
|
.get('/person')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
total: 3,
|
total: 2,
|
||||||
hidden: 1,
|
hidden: 1,
|
||||||
people: [
|
people: [expect.objectContaining({ name: 'visible_person' })],
|
||||||
expect.objectContaining({ name: 'multiple_assets_person' }),
|
|
||||||
expect.objectContaining({ name: 'visible_person' }),
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /person/:id', () => {
|
describe('GET /person/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(`/person/${uuidDto.notFound}`);
|
const { status, body } = await request(app).get(
|
||||||
|
`/person/${uuidDto.notFound}`
|
||||||
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -117,71 +107,11 @@ describe('/person', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /person/:id/statistics', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get(`/person/${multipleAssetsPerson.id}/statistics`);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error if person with id does not exist', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/person/${uuidDto.notFound}/statistics`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the correct number of assets', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/person/${multipleAssetsPerson.id}/statistics`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ assets: 2 }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /person', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post(`/person`);
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const { birthDate, response } of invalidBirthday) {
|
|
||||||
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post(`/person`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ birthDate });
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(response));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should create a person', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post(`/person`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({
|
|
||||||
name: 'New Person',
|
|
||||||
birthDate: '1990-01-01T05:00:00.000Z',
|
|
||||||
});
|
|
||||||
expect(status).toBe(201);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
id: expect.any(String),
|
|
||||||
name: 'New Person',
|
|
||||||
birthDate: '1990-01-01T05:00:00.000Z',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('PUT /person/:id', () => {
|
describe('PUT /person/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(`/person/${uuidDto.notFound}`);
|
const { status, body } = await request(app).put(
|
||||||
|
`/person/${uuidDto.notFound}`
|
||||||
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -201,16 +131,24 @@ describe('/person', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { birthDate, response } of invalidBirthday) {
|
it('should not accept invalid birth dates', async () => {
|
||||||
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
|
for (const { birthDate, response } of [
|
||||||
|
{ birthDate: false, response: 'Not found or no person.write access' },
|
||||||
|
{ birthDate: 'false', response: ['birthDate must be a Date instance'] },
|
||||||
|
{
|
||||||
|
birthDate: '123567',
|
||||||
|
response: 'Not found or no person.write access',
|
||||||
|
},
|
||||||
|
{ birthDate: 123567, response: 'Not found or no person.write access' },
|
||||||
|
]) {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/person/${visiblePerson.id}`)
|
.put(`/person/${uuidDto.notFound}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ birthDate });
|
.send({ birthDate });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(response));
|
expect(body).toEqual(errorDto.badRequest(response));
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
it('should update a date of birth', async () => {
|
it('should update a date of birth', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -222,8 +160,15 @@ describe('/person', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should clear a date of birth', async () => {
|
it('should clear a date of birth', async () => {
|
||||||
|
// TODO ironically this uses the update endpoint to create the person
|
||||||
|
const person = await apiUtils.createPerson(admin.accessToken, {
|
||||||
|
birthDate: new Date('1990-01-01').toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(person.birthDate).toBeDefined();
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/person/${visiblePerson.id}`)
|
.put(`/person/${person.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ birthDate: null });
|
.send({ birthDate: null });
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
|
|||||||
@@ -1,451 +0,0 @@
|
|||||||
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets } from '@immich/sdk';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { readFile } from 'node:fs/promises';
|
|
||||||
import { join } from 'node:path';
|
|
||||||
import { Socket } from 'socket.io-client';
|
|
||||||
import { errorDto } from 'src/responses';
|
|
||||||
import { app, asBearerAuth, testAssetDir, utils } from 'src/utils';
|
|
||||||
import request from 'supertest';
|
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
const today = DateTime.now();
|
|
||||||
|
|
||||||
describe('/search', () => {
|
|
||||||
let admin: LoginResponseDto;
|
|
||||||
let websocket: Socket;
|
|
||||||
|
|
||||||
let assetFalcon: AssetFileUploadResponseDto;
|
|
||||||
let assetDenali: AssetFileUploadResponseDto;
|
|
||||||
let assetCyclamen: AssetFileUploadResponseDto;
|
|
||||||
let assetNotocactus: AssetFileUploadResponseDto;
|
|
||||||
let assetSilver: AssetFileUploadResponseDto;
|
|
||||||
// let assetDensity: AssetFileUploadResponseDto;
|
|
||||||
// let assetPhiladelphia: AssetFileUploadResponseDto;
|
|
||||||
// let assetOrychophragmus: AssetFileUploadResponseDto;
|
|
||||||
// let assetRidge: AssetFileUploadResponseDto;
|
|
||||||
// let assetPolemonium: AssetFileUploadResponseDto;
|
|
||||||
// let assetWood: AssetFileUploadResponseDto;
|
|
||||||
let assetHeic: AssetFileUploadResponseDto;
|
|
||||||
let assetRocks: AssetFileUploadResponseDto;
|
|
||||||
let assetOneJpg6: AssetFileUploadResponseDto;
|
|
||||||
let assetOneHeic6: AssetFileUploadResponseDto;
|
|
||||||
let assetOneJpg5: AssetFileUploadResponseDto;
|
|
||||||
let assetGlarus: AssetFileUploadResponseDto;
|
|
||||||
let assetSprings: AssetFileUploadResponseDto;
|
|
||||||
let assetLast: AssetFileUploadResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await utils.resetDatabase();
|
|
||||||
admin = await utils.adminSetup();
|
|
||||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
|
||||||
|
|
||||||
const files = [
|
|
||||||
{ filename: '/albums/nature/prairie_falcon.jpg' },
|
|
||||||
{ filename: '/formats/webp/denali.webp' },
|
|
||||||
{ filename: '/albums/nature/cyclamen_persicum.jpg', dto: { isFavorite: true } },
|
|
||||||
{ filename: '/albums/nature/notocactus_minimus.jpg' },
|
|
||||||
{ filename: '/albums/nature/silver_fir.jpg' },
|
|
||||||
{ filename: '/formats/heic/IMG_2682.heic' },
|
|
||||||
{ filename: '/formats/jpg/el_torcal_rocks.jpg' },
|
|
||||||
{ filename: '/formats/motionphoto/Samsung One UI 6.jpg' },
|
|
||||||
{ filename: '/formats/motionphoto/Samsung One UI 6.heic' },
|
|
||||||
{ filename: '/formats/motionphoto/Samsung One UI 5.jpg' },
|
|
||||||
{ filename: '/formats/raw/Nikon/D80/glarus.nef', dto: { isReadOnly: true } },
|
|
||||||
{ filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } },
|
|
||||||
|
|
||||||
// used for search suggestions
|
|
||||||
{ filename: '/formats/png/density_plot.png' },
|
|
||||||
{ filename: '/formats/raw/Nikon/D700/philadelphia.nef' },
|
|
||||||
{ filename: '/albums/nature/orychophragmus_violaceus.jpg' },
|
|
||||||
{ filename: '/albums/nature/tanners_ridge.jpg' },
|
|
||||||
{ filename: '/albums/nature/polemonium_reptans.jpg' },
|
|
||||||
|
|
||||||
// last asset
|
|
||||||
{ filename: '/albums/nature/wood_anemones.jpg' },
|
|
||||||
];
|
|
||||||
const assets: AssetFileUploadResponseDto[] = [];
|
|
||||||
for (const { filename, dto } of files) {
|
|
||||||
const bytes = await readFile(join(testAssetDir, filename));
|
|
||||||
assets.push(
|
|
||||||
await utils.createAsset(admin.accessToken, {
|
|
||||||
deviceAssetId: `test-${filename}`,
|
|
||||||
assetData: { bytes, filename },
|
|
||||||
...dto,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const asset of assets) {
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
[
|
|
||||||
assetFalcon,
|
|
||||||
assetDenali,
|
|
||||||
assetCyclamen,
|
|
||||||
assetNotocactus,
|
|
||||||
assetSilver,
|
|
||||||
assetHeic,
|
|
||||||
assetRocks,
|
|
||||||
assetOneJpg6,
|
|
||||||
assetOneHeic6,
|
|
||||||
assetOneJpg5,
|
|
||||||
assetGlarus,
|
|
||||||
assetSprings,
|
|
||||||
// assetDensity,
|
|
||||||
// assetPhiladelphia,
|
|
||||||
// assetOrychophragmus,
|
|
||||||
// assetRidge,
|
|
||||||
// assetPolemonium,
|
|
||||||
// assetWood,
|
|
||||||
] = assets;
|
|
||||||
|
|
||||||
assetLast = assets.at(-1) as AssetFileUploadResponseDto;
|
|
||||||
|
|
||||||
await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await utils.disconnectWebsocket(websocket);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /search/metadata', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post('/search/metadata');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
const badTests = [
|
|
||||||
{
|
|
||||||
should: 'should reject page as a string',
|
|
||||||
dto: { page: 'abc' },
|
|
||||||
expected: ['page must not be less than 1', 'page must be an integer number'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should reject page as a decimal',
|
|
||||||
dto: { page: 1.5 },
|
|
||||||
expected: ['page must be an integer number'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should reject page as a negative number',
|
|
||||||
dto: { page: -10 },
|
|
||||||
expected: ['page must not be less than 1'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should reject page as 0',
|
|
||||||
dto: { page: 0 },
|
|
||||||
expected: ['page must not be less than 1'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should reject size as a string',
|
|
||||||
dto: { size: 'abc' },
|
|
||||||
expected: [
|
|
||||||
'size must not be greater than 1000',
|
|
||||||
'size must not be less than 1',
|
|
||||||
'size must be an integer number',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should reject an invalid size',
|
|
||||||
dto: { size: -1.5 },
|
|
||||||
expected: ['size must not be less than 1', 'size must be an integer number'],
|
|
||||||
},
|
|
||||||
...[
|
|
||||||
'isArchived',
|
|
||||||
'isFavorite',
|
|
||||||
'isReadOnly',
|
|
||||||
'isExternal',
|
|
||||||
'isEncoded',
|
|
||||||
'isMotion',
|
|
||||||
'isOffline',
|
|
||||||
'isVisible',
|
|
||||||
].map((value) => ({
|
|
||||||
should: `should reject ${value} not a boolean`,
|
|
||||||
dto: { [value]: 'immich' },
|
|
||||||
expected: [`${value} must be a boolean value`],
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const { should, dto, expected } of badTests) {
|
|
||||||
it(should, async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/search/metadata')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send(dto);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(expected));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchTests = [
|
|
||||||
{
|
|
||||||
should: 'should get my assets',
|
|
||||||
deferred: () => ({ dto: { size: 1 }, assets: [assetLast] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should sort my assets in reverse',
|
|
||||||
deferred: () => ({ dto: { order: 'asc', size: 2 }, assets: [assetCyclamen, assetNotocactus] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should support pagination',
|
|
||||||
deferred: () => ({ dto: { order: 'asc', size: 1, page: 2 }, assets: [assetNotocactus] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by checksum (base64)',
|
|
||||||
deferred: () => ({ dto: { checksum: '9IXBDMjj9OrQb+1YMHprZJgZ/UQ=' }, assets: [assetCyclamen] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by checksum (hex)',
|
|
||||||
deferred: () => ({ dto: { checksum: 'f485c10cc8e3f4ead06fed58307a6b649819fd44' }, assets: [assetCyclamen] }),
|
|
||||||
},
|
|
||||||
{ should: 'should search by id', deferred: () => ({ dto: { id: assetCyclamen.id }, assets: [assetCyclamen] }) },
|
|
||||||
{
|
|
||||||
should: 'should search by isFavorite (true)',
|
|
||||||
deferred: () => ({ dto: { isFavorite: true }, assets: [assetCyclamen] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by isFavorite (false)',
|
|
||||||
deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by isArchived (true)',
|
|
||||||
deferred: () => ({ dto: { isArchived: true }, assets: [assetSprings] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by isArchived (false)',
|
|
||||||
deferred: () => ({ dto: { size: 1, isArchived: false }, assets: [assetLast] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by isReadOnly (true)',
|
|
||||||
deferred: () => ({ dto: { isReadOnly: true }, assets: [assetGlarus] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by isReadOnly (false)',
|
|
||||||
deferred: () => ({ dto: { size: 1, isReadOnly: false }, assets: [assetLast] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by type (image)',
|
|
||||||
deferred: () => ({ dto: { size: 1, type: 'IMAGE' }, assets: [assetLast] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by type (video)',
|
|
||||||
deferred: () => ({
|
|
||||||
dto: { type: 'VIDEO' },
|
|
||||||
assets: [
|
|
||||||
// the three live motion photos
|
|
||||||
{ id: expect.any(String) },
|
|
||||||
{ id: expect.any(String) },
|
|
||||||
{ id: expect.any(String) },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by trashedBefore',
|
|
||||||
deferred: () => ({ dto: { trashedBefore: today.plus({ hour: 1 }).toJSDate() }, assets: [assetSilver] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by trashedBefore (no results)',
|
|
||||||
deferred: () => ({ dto: { trashedBefore: today.minus({ days: 1 }).toJSDate() }, assets: [] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by trashedAfter',
|
|
||||||
deferred: () => ({ dto: { trashedAfter: today.minus({ hour: 1 }).toJSDate() }, assets: [assetSilver] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by trashedAfter (no results)',
|
|
||||||
deferred: () => ({ dto: { trashedAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by takenBefore',
|
|
||||||
deferred: () => ({ dto: { size: 1, takenBefore: today.plus({ hour: 1 }).toJSDate() }, assets: [assetLast] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by takenBefore (no results)',
|
|
||||||
deferred: () => ({ dto: { takenBefore: DateTime.fromObject({ year: 1234 }).toJSDate() }, assets: [] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by takenAfter',
|
|
||||||
deferred: () => ({
|
|
||||||
dto: { size: 1, takenAfter: DateTime.fromObject({ year: 1234 }).toJSDate() },
|
|
||||||
assets: [assetLast],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by takenAfter (no results)',
|
|
||||||
deferred: () => ({ dto: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }),
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// should: 'should search by originalPath',
|
|
||||||
// deferred: () => ({
|
|
||||||
// dto: { originalPath: asset1.originalPath },
|
|
||||||
// assets: [asset1],
|
|
||||||
// }),
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
should: 'should search by originalFilename',
|
|
||||||
deferred: () => ({
|
|
||||||
dto: { originalFileName: 'rocks' },
|
|
||||||
assets: [assetRocks],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by originalFilename with spaces',
|
|
||||||
deferred: () => ({
|
|
||||||
dto: { originalFileName: 'Samsung One', type: 'IMAGE' },
|
|
||||||
assets: [assetOneJpg5, assetOneJpg6, assetOneHeic6],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by city',
|
|
||||||
deferred: () => ({ dto: { city: 'Ralston' }, assets: [assetHeic] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by state',
|
|
||||||
deferred: () => ({ dto: { state: 'Douglas County, Nebraska' }, assets: [assetHeic] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by country',
|
|
||||||
deferred: () => ({ dto: { country: 'United States of America' }, assets: [assetHeic] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by make',
|
|
||||||
deferred: () => ({ dto: { make: 'Canon' }, assets: [assetFalcon, assetDenali] }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
should: 'should search by model',
|
|
||||||
deferred: () => ({ dto: { model: 'Canon EOS 7D' }, assets: [assetDenali] }),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const { should, deferred } of searchTests) {
|
|
||||||
it(should, async () => {
|
|
||||||
const { assets, dto } = deferred();
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/search/metadata')
|
|
||||||
.send(dto)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body.assets).toBeDefined();
|
|
||||||
expect(Array.isArray(body.assets.items)).toBe(true);
|
|
||||||
for (const [i, asset] of assets.entries()) {
|
|
||||||
expect(body.assets.items[i]).toEqual(expect.objectContaining({ id: asset.id }));
|
|
||||||
}
|
|
||||||
expect(body.assets.items).toHaveLength(assets.length);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /search/smart', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post('/search/smart');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /search/explore', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/search/explore');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get explore data', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/search/explore')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual([
|
|
||||||
{ fieldName: 'exifInfo.city', items: [] },
|
|
||||||
{ fieldName: 'smartInfo.tags', items: [] },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /search/places', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/search/places');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get places', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/search/places?name=Paris')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(Array.isArray(body)).toBe(true);
|
|
||||||
expect(body.length).toBeGreaterThan(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /search/suggestions', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/search/suggestions');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get suggestions for country', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/search/suggestions?type=country')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(body).toEqual(['United States of America']);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get suggestions for state', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/search/suggestions?type=state')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(body).toEqual(['Douglas County, Nebraska', 'Mesa County, Colorado']);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get suggestions for city', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/search/suggestions?type=city')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(body).toEqual(['Palisade', 'Ralston']);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get suggestions for camera make', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/search/suggestions?type=camera-make')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(body).toEqual([
|
|
||||||
'Apple',
|
|
||||||
'Canon',
|
|
||||||
'FUJIFILM',
|
|
||||||
'NIKON CORPORATION',
|
|
||||||
'PENTAX Corporation',
|
|
||||||
'samsung',
|
|
||||||
'SONY',
|
|
||||||
]);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get suggestions for camera model', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/search/suggestions?type=camera-model')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(body).toEqual([
|
|
||||||
'Canon EOS 7D',
|
|
||||||
'Canon EOS R5',
|
|
||||||
'DSLR-A550',
|
|
||||||
'FinePix S3Pro',
|
|
||||||
'iPhone 7',
|
|
||||||
'NIKON D700',
|
|
||||||
'NIKON D750',
|
|
||||||
'NIKON D80',
|
|
||||||
'PENTAX K10D',
|
|
||||||
'SM-F711N',
|
|
||||||
'SM-S906U',
|
|
||||||
'SM-T970',
|
|
||||||
]);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
|
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, utils } from 'src/utils';
|
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -10,9 +10,10 @@ describe('/server-info', () => {
|
|||||||
let nonAdmin: LoginResponseDto;
|
let nonAdmin: LoginResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
admin = await utils.adminSetup({ onboarding: false });
|
await dbUtils.reset();
|
||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
admin = await apiUtils.adminSetup({ onboarding: false });
|
||||||
|
nonAdmin = await apiUtils.userSetup(admin.accessToken, createUserDto.user1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /server-info', () => {
|
describe('GET /server-info', () => {
|
||||||
@@ -87,7 +88,6 @@ describe('/server-info', () => {
|
|||||||
loginPageMessage: '',
|
loginPageMessage: '',
|
||||||
oauthButtonText: 'Login with OAuth',
|
oauthButtonText: 'Login with OAuth',
|
||||||
trashDays: 30,
|
trashDays: 30,
|
||||||
userDeleteDelay: 7,
|
|
||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
isOnboarded: false,
|
isOnboarded: false,
|
||||||
@@ -97,7 +97,9 @@ describe('/server-info', () => {
|
|||||||
|
|
||||||
describe('GET /server-info/statistics', () => {
|
describe('GET /server-info/statistics', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/server-info/statistics');
|
const { status, body } = await request(app).get(
|
||||||
|
'/server-info/statistics'
|
||||||
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -143,7 +145,9 @@ describe('/server-info', () => {
|
|||||||
|
|
||||||
describe('GET /server-info/media-types', () => {
|
describe('GET /server-info/media-types', () => {
|
||||||
it('should return accepted media types', async () => {
|
it('should return accepted media types', async () => {
|
||||||
const { status, body } = await request(app).get('/server-info/media-types');
|
const { status, body } = await request(app).get(
|
||||||
|
'/server-info/media-types'
|
||||||
|
);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
sidecar: ['.xmp'],
|
sidecar: ['.xmp'],
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import {
|
import {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AssetFileUploadResponseDto,
|
AssetResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
|
SharedLinkCreateDto,
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
|
createSharedLink as create,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('/shared-link', () => {
|
describe('/shared-link', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let asset1: AssetFileUploadResponseDto;
|
let asset1: AssetResponseDto;
|
||||||
let asset2: AssetFileUploadResponseDto;
|
let asset2: AssetResponseDto;
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user2: LoginResponseDto;
|
let user2: LoginResponseDto;
|
||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
@@ -30,20 +32,30 @@ describe('/shared-link', () => {
|
|||||||
let linkWithoutMetadata: SharedLinkResponseDto;
|
let linkWithoutMetadata: SharedLinkResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
|
await dbUtils.reset();
|
||||||
|
|
||||||
admin = await utils.adminSetup();
|
admin = await apiUtils.adminSetup();
|
||||||
|
|
||||||
[user1, user2] = await Promise.all([
|
[user1, user2] = await Promise.all([
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user1),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user2),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
[asset1, asset2] = await Promise.all([utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken)]);
|
[asset1, asset2] = await Promise.all([
|
||||||
|
apiUtils.createAsset(user1.accessToken),
|
||||||
|
apiUtils.createAsset(user1.accessToken),
|
||||||
|
]);
|
||||||
|
|
||||||
[album, deletedAlbum, metadataAlbum] = await Promise.all([
|
[album, deletedAlbum, metadataAlbum] = await Promise.all([
|
||||||
createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }),
|
createAlbum(
|
||||||
createAlbum({ createAlbumDto: { albumName: 'deleted album' } }, { headers: asBearerAuth(user2.accessToken) }),
|
{ createAlbumDto: { albumName: 'album' } },
|
||||||
|
{ headers: asBearerAuth(user1.accessToken) }
|
||||||
|
),
|
||||||
|
createAlbum(
|
||||||
|
{ createAlbumDto: { albumName: 'deleted album' } },
|
||||||
|
{ headers: asBearerAuth(user2.accessToken) }
|
||||||
|
),
|
||||||
createAlbum(
|
createAlbum(
|
||||||
{
|
{
|
||||||
createAlbumDto: {
|
createAlbumDto: {
|
||||||
@@ -51,42 +63,51 @@ describe('/shared-link', () => {
|
|||||||
assetIds: [asset1.id],
|
assetIds: [asset1.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(user1.accessToken) },
|
{ headers: asBearerAuth(user1.accessToken) }
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
[linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] =
|
[
|
||||||
await Promise.all([
|
linkWithDeletedAlbum,
|
||||||
utils.createSharedLink(user2.accessToken, {
|
linkWithAlbum,
|
||||||
type: SharedLinkType.Album,
|
linkWithAssets,
|
||||||
albumId: deletedAlbum.id,
|
linkWithPassword,
|
||||||
}),
|
linkWithMetadata,
|
||||||
utils.createSharedLink(user1.accessToken, {
|
linkWithoutMetadata,
|
||||||
type: SharedLinkType.Album,
|
] = await Promise.all([
|
||||||
albumId: album.id,
|
apiUtils.createSharedLink(user2.accessToken, {
|
||||||
}),
|
type: SharedLinkType.Album,
|
||||||
utils.createSharedLink(user1.accessToken, {
|
albumId: deletedAlbum.id,
|
||||||
type: SharedLinkType.Individual,
|
}),
|
||||||
assetIds: [asset1.id],
|
apiUtils.createSharedLink(user1.accessToken, {
|
||||||
}),
|
type: SharedLinkType.Album,
|
||||||
utils.createSharedLink(user1.accessToken, {
|
albumId: album.id,
|
||||||
type: SharedLinkType.Album,
|
}),
|
||||||
albumId: album.id,
|
apiUtils.createSharedLink(user1.accessToken, {
|
||||||
password: 'foo',
|
type: SharedLinkType.Individual,
|
||||||
}),
|
assetIds: [asset1.id],
|
||||||
utils.createSharedLink(user1.accessToken, {
|
}),
|
||||||
type: SharedLinkType.Album,
|
apiUtils.createSharedLink(user1.accessToken, {
|
||||||
albumId: metadataAlbum.id,
|
type: SharedLinkType.Album,
|
||||||
showMetadata: true,
|
albumId: album.id,
|
||||||
}),
|
password: 'foo',
|
||||||
utils.createSharedLink(user1.accessToken, {
|
}),
|
||||||
type: SharedLinkType.Album,
|
apiUtils.createSharedLink(user1.accessToken, {
|
||||||
albumId: metadataAlbum.id,
|
type: SharedLinkType.Album,
|
||||||
showMetadata: false,
|
albumId: metadataAlbum.id,
|
||||||
}),
|
showMetadata: true,
|
||||||
]);
|
}),
|
||||||
|
apiUtils.createSharedLink(user1.accessToken, {
|
||||||
|
type: SharedLinkType.Album,
|
||||||
|
albumId: metadataAlbum.id,
|
||||||
|
showMetadata: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
await deleteUser({ id: user2.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
await deleteUser(
|
||||||
|
{ id: user2.userId },
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /shared-link', () => {
|
describe('GET /shared-link', () => {
|
||||||
@@ -111,7 +132,7 @@ describe('/shared-link', () => {
|
|||||||
expect.objectContaining({ id: linkWithPassword.id }),
|
expect.objectContaining({ id: linkWithPassword.id }),
|
||||||
expect.objectContaining({ id: linkWithMetadata.id }),
|
expect.objectContaining({ id: linkWithMetadata.id }),
|
||||||
expect.objectContaining({ id: linkWithoutMetadata.id }),
|
expect.objectContaining({ id: linkWithoutMetadata.id }),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,13 +148,17 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
describe('GET /shared-link/me', () => {
|
describe('GET /shared-link/me', () => {
|
||||||
it('should not require admin authentication', async () => {
|
it('should not require admin authentication', async () => {
|
||||||
const { status } = await request(app).get('/shared-link/me').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status } = await request(app)
|
||||||
|
.get('/shared-link/me')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(403);
|
expect(status).toBe(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get data for correct shared link', async () => {
|
it('should get data for correct shared link', async () => {
|
||||||
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithAlbum.key });
|
const { status, body } = await request(app)
|
||||||
|
.get('/shared-link/me')
|
||||||
|
.query({ key: linkWithAlbum.key });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
@@ -141,7 +166,7 @@ describe('/shared-link', () => {
|
|||||||
album,
|
album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -155,14 +180,18 @@ describe('/shared-link', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return unauthorized if target has been soft deleted', async () => {
|
it('should return unauthorized if target has been soft deleted', async () => {
|
||||||
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key });
|
const { status, body } = await request(app)
|
||||||
|
.get('/shared-link/me')
|
||||||
|
.query({ key: linkWithDeletedAlbum.key });
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.invalidShareKey);
|
expect(body).toEqual(errorDto.invalidShareKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return unauthorized for password protected link', async () => {
|
it('should return unauthorized for password protected link', async () => {
|
||||||
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithPassword.key });
|
const { status, body } = await request(app)
|
||||||
|
.get('/shared-link/me')
|
||||||
|
.query({ key: linkWithPassword.key });
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.invalidSharePassword);
|
expect(body).toEqual(errorDto.invalidSharePassword);
|
||||||
@@ -179,28 +208,32 @@ describe('/shared-link', () => {
|
|||||||
album,
|
album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return metadata for album shared link', async () => {
|
it('should return metadata for album shared link', async () => {
|
||||||
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithMetadata.key });
|
const { status, body } = await request(app)
|
||||||
|
.get('/shared-link/me')
|
||||||
|
.query({ key: linkWithMetadata.key });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body.assets).toHaveLength(1);
|
expect(body.assets).toHaveLength(1);
|
||||||
expect(body.assets[0]).toEqual(
|
expect(body.assets[0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
originalFileName: 'example.png',
|
originalFileName: 'example',
|
||||||
localDateTime: expect.any(String),
|
localDateTime: expect.any(String),
|
||||||
fileCreatedAt: expect.any(String),
|
fileCreatedAt: expect.any(String),
|
||||||
exifInfo: expect.any(Object),
|
exifInfo: expect.any(Object),
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
expect(body.album).toBeDefined();
|
expect(body.album).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not return metadata for album shared link without metadata', async () => {
|
it('should not return metadata for album shared link without metadata', async () => {
|
||||||
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithoutMetadata.key });
|
const { status, body } = await request(app)
|
||||||
|
.get('/shared-link/me')
|
||||||
|
.query({ key: linkWithoutMetadata.key });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body.assets).toHaveLength(1);
|
expect(body.assets).toHaveLength(1);
|
||||||
@@ -216,7 +249,9 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
describe('GET /shared-link/:id', () => {
|
describe('GET /shared-link/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(`/shared-link/${linkWithAlbum.id}`);
|
const { status, body } = await request(app).get(
|
||||||
|
`/shared-link/${linkWithAlbum.id}`
|
||||||
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -233,7 +268,7 @@ describe('/shared-link', () => {
|
|||||||
album,
|
album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -243,7 +278,9 @@ describe('/shared-link', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' }));
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({ message: 'Shared link not found' })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -273,7 +310,9 @@ describe('/shared-link', () => {
|
|||||||
.send({ type: SharedLinkType.Album });
|
.send({ type: SharedLinkType.Album });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(expect.objectContaining({ message: 'Invalid albumId' }));
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({ message: 'Invalid albumId' })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a valid asset id', async () => {
|
it('should require a valid asset id', async () => {
|
||||||
@@ -283,7 +322,9 @@ describe('/shared-link', () => {
|
|||||||
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
|
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(expect.objectContaining({ message: 'Invalid assetIds' }));
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({ message: 'Invalid assetIds' })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a shared link', async () => {
|
it('should create a shared link', async () => {
|
||||||
@@ -297,7 +338,7 @@ describe('/shared-link', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -334,7 +375,7 @@ describe('/shared-link', () => {
|
|||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
description: 'foo',
|
description: 'foo',
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -385,7 +426,9 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
describe('DELETE /shared-link/:id', () => {
|
describe('DELETE /shared-link/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(`/shared-link/${linkWithAlbum.id}`);
|
const { status, body } = await request(app).delete(
|
||||||
|
`/shared-link/${linkWithAlbum.id}`
|
||||||
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LoginResponseDto } from '@immich/sdk';
|
import { LoginResponseDto } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, utils } from 'src/utils';
|
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -10,14 +10,17 @@ describe('/system-config', () => {
|
|||||||
let nonAdmin: LoginResponseDto;
|
let nonAdmin: LoginResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
admin = await utils.adminSetup();
|
await dbUtils.reset();
|
||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
admin = await apiUtils.adminSetup();
|
||||||
|
nonAdmin = await apiUtils.userSetup(admin.accessToken, createUserDto.user1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /system-config/map/style.json', () => {
|
describe('GET /system-config/map/style.json', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/system-config/map/style.json');
|
const { status, body } = await request(app).get(
|
||||||
|
'/system-config/map/style.json'
|
||||||
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -29,7 +32,11 @@ describe('/system-config', () => {
|
|||||||
.query({ theme })
|
.query({ theme })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest([
|
||||||
|
'theme must be one of the following values: light, dark',
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import { LoginResponseDto, getAllAssets } from '@immich/sdk';
|
|
||||||
import { Socket } from 'socket.io-client';
|
|
||||||
import { errorDto } from 'src/responses';
|
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
|
||||||
import request from 'supertest';
|
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
describe('/trash', () => {
|
|
||||||
let admin: LoginResponseDto;
|
|
||||||
let ws: Socket;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await utils.resetDatabase();
|
|
||||||
admin = await utils.adminSetup({ onboarding: false });
|
|
||||||
ws = await utils.connectWebsocket(admin.accessToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
utils.disconnectWebsocket(ws);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /trash/empty', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post('/trash/empty');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should empty the trash', async () => {
|
|
||||||
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
|
||||||
await utils.deleteAssets(admin.accessToken, [assetId]);
|
|
||||||
|
|
||||||
const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
|
|
||||||
expect(before.length).toBeGreaterThanOrEqual(1);
|
|
||||||
|
|
||||||
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
|
|
||||||
|
|
||||||
const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
expect(after.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /trash/restore', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post('/trash/restore');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should restore all trashed assets', async () => {
|
|
||||||
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
|
||||||
await utils.deleteAssets(admin.accessToken, [assetId]);
|
|
||||||
|
|
||||||
const before = await utils.getAssetInfo(admin.accessToken, assetId);
|
|
||||||
expect(before.isTrashed).toBe(true);
|
|
||||||
|
|
||||||
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
const after = await utils.getAssetInfo(admin.accessToken, assetId);
|
|
||||||
expect(after.isTrashed).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /trash/restore/assets', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post('/trash/restore/assets');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should restore a trashed asset by id', async () => {
|
|
||||||
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
|
||||||
await utils.deleteAssets(admin.accessToken, [assetId]);
|
|
||||||
|
|
||||||
const before = await utils.getAssetInfo(admin.accessToken, assetId);
|
|
||||||
expect(before.isTrashed).toBe(true);
|
|
||||||
|
|
||||||
const { status } = await request(app)
|
|
||||||
.post('/trash/restore/assets')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ ids: [assetId] });
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
const after = await utils.getAssetInfo(admin.accessToken, assetId);
|
|
||||||
expect(after.isTrashed).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,37 +1,31 @@
|
|||||||
import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk';
|
import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk';
|
||||||
import { Socket } from 'socket.io-client';
|
|
||||||
import { createUserDto, userDto } from 'src/fixtures';
|
import { createUserDto, userDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('/user', () => {
|
|
||||||
let websocket: Socket;
|
|
||||||
|
|
||||||
|
describe('/server-info', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let deletedUser: LoginResponseDto;
|
let deletedUser: LoginResponseDto;
|
||||||
let userToDelete: LoginResponseDto;
|
let userToDelete: LoginResponseDto;
|
||||||
let userToHardDelete: LoginResponseDto;
|
|
||||||
let nonAdmin: LoginResponseDto;
|
let nonAdmin: LoginResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
admin = await utils.adminSetup({ onboarding: false });
|
await dbUtils.reset();
|
||||||
|
admin = await apiUtils.adminSetup({ onboarding: false });
|
||||||
|
|
||||||
[websocket, deletedUser, nonAdmin, userToDelete, userToHardDelete] = await Promise.all([
|
[deletedUser, nonAdmin, userToDelete] = await Promise.all([
|
||||||
utils.connectWebsocket(admin.accessToken),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user1),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user2),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user3),
|
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user4),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await deleteUser({ id: deletedUser.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
await deleteUser(
|
||||||
});
|
{ id: deletedUser.userId },
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
afterAll(() => {
|
);
|
||||||
utils.disconnectWebsocket(websocket);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /user', () => {
|
describe('GET /user', () => {
|
||||||
@@ -42,17 +36,18 @@ describe('/user', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get users', async () => {
|
it('should get users', async () => {
|
||||||
const { status, body } = await request(app).get('/user').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app)
|
||||||
|
.get('/user')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
expect(body).toHaveLength(5);
|
expect(body).toHaveLength(4);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({ email: 'admin@immich.cloud' }),
|
expect.objectContaining({ email: 'admin@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user1@immich.cloud' }),
|
expect.objectContaining({ email: 'user1@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user4@immich.cloud' }),
|
])
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,14 +57,13 @@ describe('/user', () => {
|
|||||||
.query({ isAll: true })
|
.query({ isAll: true })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toHaveLength(4);
|
expect(body).toHaveLength(3);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({ email: 'admin@immich.cloud' }),
|
expect.objectContaining({ email: 'admin@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user4@immich.cloud' }),
|
])
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,15 +74,14 @@ describe('/user', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toHaveLength(5);
|
expect(body).toHaveLength(4);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({ email: 'admin@immich.cloud' }),
|
expect.objectContaining({ email: 'admin@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user1@immich.cloud' }),
|
expect.objectContaining({ email: 'user1@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user4@immich.cloud' }),
|
])
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -119,7 +112,9 @@ describe('/user', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get my info', async () => {
|
it('should get my info', async () => {
|
||||||
const { status, body } = await request(app).get(`/user/me`).set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app)
|
||||||
|
.get(`/user/me`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
id: admin.userId,
|
id: admin.userId,
|
||||||
@@ -130,7 +125,9 @@ describe('/user', () => {
|
|||||||
|
|
||||||
describe('POST /user', () => {
|
describe('POST /user', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(`/user`).send(createUserDto.user1);
|
const { status, body } = await request(app)
|
||||||
|
.post(`/user`)
|
||||||
|
.send(createUserDto.user1);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -151,13 +148,13 @@ describe('/user', () => {
|
|||||||
.post(`/user`)
|
.post(`/user`)
|
||||||
.send({
|
.send({
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
email: 'user5@immich.cloud',
|
email: 'user4@immich.cloud',
|
||||||
password: 'password123',
|
password: 'password123',
|
||||||
name: 'Immich',
|
name: 'Immich',
|
||||||
})
|
})
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
email: 'user5@immich.cloud',
|
email: 'user4@immich.cloud',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
});
|
});
|
||||||
@@ -184,7 +181,9 @@ describe('/user', () => {
|
|||||||
|
|
||||||
describe('DELETE /user/:id', () => {
|
describe('DELETE /user/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(`/user/${userToDelete.userId}`);
|
const { status, body } = await request(app).delete(
|
||||||
|
`/user/${userToDelete.userId}`
|
||||||
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -201,22 +200,6 @@ describe('/user', () => {
|
|||||||
deletedAt: expect.any(String),
|
deletedAt: expect.any(String),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hard delete user', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.delete(`/user/${userToHardDelete.userId}`)
|
|
||||||
.send({ force: true })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
id: userToHardDelete.userId,
|
|
||||||
updatedAt: expect.any(String),
|
|
||||||
deletedAt: expect.any(String),
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'userDelete', id: userToHardDelete.userId, timeout: 5000 });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /user', () => {
|
describe('PUT /user', () => {
|
||||||
@@ -258,7 +241,10 @@ describe('/user', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
|
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
|
||||||
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getUserById(
|
||||||
|
{ id: admin.userId },
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
|
);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/user`)
|
.put(`/user`)
|
||||||
@@ -275,7 +261,10 @@ describe('/user', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update first and last name', async () => {
|
it('should update first and last name', async () => {
|
||||||
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getUserById(
|
||||||
|
{ id: admin.userId },
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
|
);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/user`)
|
.put(`/user`)
|
||||||
@@ -295,7 +284,10 @@ describe('/user', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update memories enabled', async () => {
|
it('should update memories enabled', async () => {
|
||||||
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getUserById(
|
||||||
|
{ id: admin.userId },
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
|
);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/user`)
|
.put(`/user`)
|
||||||
.send({
|
.send({
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { stat } from 'node:fs/promises';
|
import { stat } from 'node:fs/promises';
|
||||||
import { app, immichCli, utils } from 'src/utils';
|
import { apiUtils, app, dbUtils, immichCli } from 'src/utils';
|
||||||
import { beforeEach, describe, expect, it } from 'vitest';
|
import { beforeEach, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe(`immich login-key`, () => {
|
describe(`immich login-key`, () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
apiUtils.setup();
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase();
|
await dbUtils.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a url', async () => {
|
it('should require a url', async () => {
|
||||||
@@ -20,17 +24,27 @@ describe(`immich login-key`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require a valid key', async () => {
|
it('should require a valid key', async () => {
|
||||||
const { stderr, exitCode } = await immichCli(['login-key', app, 'immich-is-so-cool']);
|
const { stderr, exitCode } = await immichCli([
|
||||||
expect(stderr).toContain('Failed to connect to server http://127.0.0.1:2283/api: Error: 401');
|
'login-key',
|
||||||
|
app,
|
||||||
|
'immich-is-so-cool',
|
||||||
|
]);
|
||||||
|
expect(stderr).toContain(
|
||||||
|
'Failed to connect to server http://127.0.0.1:2283/api: Error: 401'
|
||||||
|
);
|
||||||
expect(exitCode).toBe(1);
|
expect(exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should login and save auth.yml with 600', async () => {
|
it('should login', async () => {
|
||||||
const admin = await utils.adminSetup();
|
const admin = await apiUtils.adminSetup();
|
||||||
const key = await utils.createApiKey(admin.accessToken);
|
const key = await apiUtils.createApiKey(admin.accessToken);
|
||||||
const { stdout, stderr, exitCode } = await immichCli(['login-key', app, `${key.secret}`]);
|
const { stdout, stderr, exitCode } = await immichCli([
|
||||||
|
'login-key',
|
||||||
|
app,
|
||||||
|
`${key.secret}`,
|
||||||
|
]);
|
||||||
expect(stdout.split('\n')).toEqual([
|
expect(stdout.split('\n')).toEqual([
|
||||||
'Logging in to http://127.0.0.1:2283/api',
|
'Logging in...',
|
||||||
'Logged in as admin@immich.cloud',
|
'Logged in as admin@immich.cloud',
|
||||||
'Wrote auth info to /tmp/immich/auth.yml',
|
'Wrote auth info to /tmp/immich/auth.yml',
|
||||||
]);
|
]);
|
||||||
@@ -41,18 +55,4 @@ describe(`immich login-key`, () => {
|
|||||||
const mode = (stats.mode & 0o777).toString(8);
|
const mode = (stats.mode & 0o777).toString(8);
|
||||||
expect(mode).toEqual('600');
|
expect(mode).toEqual('600');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should login without /api in the url', async () => {
|
|
||||||
const admin = await utils.adminSetup();
|
|
||||||
const key = await utils.createApiKey(admin.accessToken);
|
|
||||||
const { stdout, stderr, exitCode } = await immichCli(['login-key', app.replaceAll('/api', ''), `${key.secret}`]);
|
|
||||||
expect(stdout.split('\n')).toEqual([
|
|
||||||
'Logging in to http://127.0.0.1:2283',
|
|
||||||
'Discovered API at http://127.0.0.1:2283/api',
|
|
||||||
'Logged in as admin@immich.cloud',
|
|
||||||
'Wrote auth info to /tmp/immich/auth.yml',
|
|
||||||
]);
|
|
||||||
expect(stderr).toBe('');
|
|
||||||
expect(exitCode).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { immichCli, utils } from 'src/utils';
|
import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe(`immich server-info`, () => {
|
describe(`immich server-info`, () => {
|
||||||
beforeAll(async () => {
|
beforeAll(() => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
await utils.cliLogin();
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await dbUtils.reset();
|
||||||
|
await cliUtils.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the server info', async () => {
|
it('should return the server info', async () => {
|
||||||
|
|||||||
@@ -1,26 +1,39 @@
|
|||||||
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
||||||
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
|
import {
|
||||||
import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
|
apiUtils,
|
||||||
|
asKeyAuth,
|
||||||
|
cliUtils,
|
||||||
|
dbUtils,
|
||||||
|
immichCli,
|
||||||
|
testAssetDir,
|
||||||
|
} from 'src/utils';
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import { mkdir, readdir, rm, symlink } from 'fs/promises';
|
||||||
|
|
||||||
describe(`immich upload`, () => {
|
describe(`immich upload`, () => {
|
||||||
let key: string;
|
let key: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(() => {
|
||||||
await utils.resetDatabase();
|
apiUtils.setup();
|
||||||
key = await utils.cliLogin();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase(['assets', 'albums']);
|
await dbUtils.reset();
|
||||||
|
key = await cliUtils.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('immich upload --recursive', () => {
|
describe('immich upload --recursive', () => {
|
||||||
it('should upload a folder recursively', async () => {
|
it('should upload a folder recursively', async () => {
|
||||||
const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
|
const { stderr, stdout, exitCode } = await immichCli([
|
||||||
|
'upload',
|
||||||
|
`${testAssetDir}/albums/nature/`,
|
||||||
|
'--recursive',
|
||||||
|
]);
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(stdout.split('\n')).toEqual(
|
expect(stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
|
expect.arrayContaining([
|
||||||
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
|
])
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
@@ -42,7 +55,7 @@ describe(`immich upload`, () => {
|
|||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
expect.stringContaining('Successfully created 1 new album'),
|
expect.stringContaining('Successfully created 1 new album'),
|
||||||
expect.stringContaining('Successfully updated 9 assets'),
|
expect.stringContaining('Successfully updated 9 assets'),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
@@ -56,9 +69,15 @@ describe(`immich upload`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add existing assets to albums', async () => {
|
it('should add existing assets to albums', async () => {
|
||||||
const response1 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
|
const response1 = await immichCli([
|
||||||
|
'upload',
|
||||||
|
`${testAssetDir}/albums/nature/`,
|
||||||
|
'--recursive',
|
||||||
|
]);
|
||||||
expect(response1.stdout.split('\n')).toEqual(
|
expect(response1.stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
|
expect.arrayContaining([
|
||||||
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
|
])
|
||||||
);
|
);
|
||||||
expect(response1.stderr).toBe('');
|
expect(response1.stderr).toBe('');
|
||||||
expect(response1.exitCode).toBe(0);
|
expect(response1.exitCode).toBe(0);
|
||||||
@@ -69,12 +88,19 @@ describe(`immich upload`, () => {
|
|||||||
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||||
expect(albums1.length).toBe(0);
|
expect(albums1.length).toBe(0);
|
||||||
|
|
||||||
const response2 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive', '--album']);
|
const response2 = await immichCli([
|
||||||
|
'upload',
|
||||||
|
`${testAssetDir}/albums/nature/`,
|
||||||
|
'--recursive',
|
||||||
|
'--album',
|
||||||
|
]);
|
||||||
expect(response2.stdout.split('\n')).toEqual(
|
expect(response2.stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining('All assets were already uploaded, nothing to do.'),
|
expect.stringContaining(
|
||||||
|
'All assets were already uploaded, nothing to do.'
|
||||||
|
),
|
||||||
expect.stringContaining('Successfully updated 9 assets'),
|
expect.stringContaining('Successfully updated 9 assets'),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
expect(response2.stderr).toBe('');
|
expect(response2.stderr).toBe('');
|
||||||
expect(response2.exitCode).toBe(0);
|
expect(response2.exitCode).toBe(0);
|
||||||
@@ -101,7 +127,7 @@ describe(`immich upload`, () => {
|
|||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
expect.stringContaining('Successfully created 1 new album'),
|
expect.stringContaining('Successfully created 1 new album'),
|
||||||
expect.stringContaining('Successfully updated 9 assets'),
|
expect.stringContaining('Successfully updated 9 assets'),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
@@ -120,10 +146,17 @@ describe(`immich upload`, () => {
|
|||||||
await mkdir(`/tmp/albums/nature`, { recursive: true });
|
await mkdir(`/tmp/albums/nature`, { recursive: true });
|
||||||
const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
|
const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
|
||||||
for (const file of filesToLink) {
|
for (const file of filesToLink) {
|
||||||
await symlink(`${testAssetDir}/albums/nature/${file}`, `/tmp/albums/nature/${file}`);
|
await symlink(
|
||||||
|
`${testAssetDir}/albums/nature/${file}`,
|
||||||
|
`/tmp/albums/nature/${file}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { stderr, stdout, exitCode } = await immichCli(['upload', `/tmp/albums/nature`, '--delete']);
|
const { stderr, stdout, exitCode } = await immichCli([
|
||||||
|
'upload',
|
||||||
|
`/tmp/albums/nature`,
|
||||||
|
'--delete',
|
||||||
|
]);
|
||||||
|
|
||||||
const files = await readdir(`/tmp/albums/nature`);
|
const files = await readdir(`/tmp/albums/nature`);
|
||||||
await rm(`/tmp/albums/nature`, { recursive: true });
|
await rm(`/tmp/albums/nature`, { recursive: true });
|
||||||
@@ -133,7 +166,7 @@ describe(`immich upload`, () => {
|
|||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
expect.stringContaining('Deleting assets that have been uploaded'),
|
expect.stringContaining('Deleting assets that have been uploaded'),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { immichCli } from 'src/utils';
|
import { apiUtils, immichCli } from 'src/utils';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const pkg = JSON.parse(readFileSync('../cli/package.json', 'utf8'));
|
const pkg = JSON.parse(readFileSync('../cli/package.json', 'utf8'));
|
||||||
|
|
||||||
describe(`immich --version`, () => {
|
describe(`immich --version`, () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
apiUtils.setup();
|
||||||
|
});
|
||||||
|
|
||||||
describe('immich --version', () => {
|
describe('immich --version', () => {
|
||||||
it('should print the cli version', async () => {
|
it('should print the cli version', async () => {
|
||||||
const { stdout, stderr, exitCode } = await immichCli(['--version']);
|
const { stdout, stderr, exitCode } = await immichCli(['--version']);
|
||||||
|
|||||||
@@ -21,13 +21,6 @@ export const signupDto = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createUserDto = {
|
export const createUserDto = {
|
||||||
create(key: string) {
|
|
||||||
return {
|
|
||||||
email: `${key}@immich.cloud`,
|
|
||||||
name: `Generated User ${key}`,
|
|
||||||
password: `password-${key}`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
user1: {
|
user1: {
|
||||||
email: 'user1@immich.cloud',
|
email: 'user1@immich.cloud',
|
||||||
name: 'User 1',
|
name: 'User 1',
|
||||||
@@ -43,17 +36,6 @@ export const createUserDto = {
|
|||||||
name: 'User 3',
|
name: 'User 3',
|
||||||
password: 'password123',
|
password: 'password123',
|
||||||
},
|
},
|
||||||
user4: {
|
|
||||||
email: 'user4@immich.cloud',
|
|
||||||
name: 'User 4',
|
|
||||||
password: 'password123',
|
|
||||||
},
|
|
||||||
userQuota: {
|
|
||||||
email: 'user-quota@immich.cloud',
|
|
||||||
name: 'User Quota',
|
|
||||||
password: 'password-quota',
|
|
||||||
quotaSizeInBytes: 512,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userDto = {
|
export const userDto = {
|
||||||
@@ -62,6 +44,7 @@ export const userDto = {
|
|||||||
email: signupDto.admin.email,
|
email: signupDto.admin.email,
|
||||||
password: signupDto.admin.password,
|
password: signupDto.admin.password,
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
|
externalPath: null,
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
@@ -80,6 +63,7 @@ export const userDto = {
|
|||||||
email: createUserDto.user1.email,
|
email: createUserDto.user1.email,
|
||||||
password: createUserDto.user1.password,
|
password: createUserDto.user1.password,
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
|
externalPath: null,
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { PNG } from 'pngjs';
|
|
||||||
|
|
||||||
const createPNG = (r: number, g: number, b: number) => {
|
|
||||||
const image = new PNG({ width: 1, height: 1 });
|
|
||||||
image.data[0] = r;
|
|
||||||
image.data[1] = g;
|
|
||||||
image.data[2] = b;
|
|
||||||
image.data[3] = 255;
|
|
||||||
return PNG.sync.write(image);
|
|
||||||
};
|
|
||||||
|
|
||||||
function* newPngFactory() {
|
|
||||||
for (let r = 0; r < 255; r++) {
|
|
||||||
for (let g = 0; g < 255; g++) {
|
|
||||||
for (let b = 0; b < 255; b++) {
|
|
||||||
yield createPNG(r, g, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pngFactory = newPngFactory();
|
|
||||||
|
|
||||||
export const makeRandomImage = () => {
|
|
||||||
const { value } = pngFactory.next();
|
|
||||||
if (!value) {
|
|
||||||
throw new Error('Ran out of random asset data');
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
@@ -65,6 +65,7 @@ export const signupResponseDto = {
|
|||||||
name: 'Immich Admin',
|
name: 'Immich Admin',
|
||||||
email: 'admin@immich.cloud',
|
email: 'admin@immich.cloud',
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
|
externalPath: null,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
// why? lol
|
// why? lol
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
@@ -76,7 +77,6 @@ export const signupResponseDto = {
|
|||||||
memoriesEnabled: true,
|
memoriesEnabled: true,
|
||||||
quotaUsageInBytes: 0,
|
quotaUsageInBytes: 0,
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
status: 'active',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,26 @@
|
|||||||
import { exec, spawn } from 'node:child_process';
|
import { spawn, exec } from 'child_process';
|
||||||
import { setTimeout } from 'node:timers';
|
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
let _resolve: () => unknown;
|
let _resolve: () => unknown;
|
||||||
let _reject: (error: Error) => unknown;
|
const promise = new Promise<void>((resolve) => (_resolve = resolve));
|
||||||
|
|
||||||
const ready = new Promise<void>((resolve, reject) => {
|
|
||||||
_resolve = resolve;
|
|
||||||
_reject = reject;
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => _reject(new Error('Timeout starting e2e environment')), 60_000);
|
|
||||||
|
|
||||||
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
|
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
|
||||||
|
|
||||||
child.stdout.on('data', (data) => {
|
child.stdout.on('data', (data) => {
|
||||||
const input = data.toString();
|
const input = data.toString();
|
||||||
console.log(input);
|
console.log(input);
|
||||||
if (input.includes('Immich Microservices is listening')) {
|
if (input.includes('Immich Server is listening')) {
|
||||||
_resolve();
|
_resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on('data', (data) => console.log(data.toString()));
|
child.stderr.on('data', (data) => console.log(data.toString()));
|
||||||
|
|
||||||
await ready;
|
await promise;
|
||||||
clearTimeout(timeout);
|
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
await new Promise<void>((resolve) => exec('docker compose down', () => resolve()));
|
await new Promise<void>((resolve) =>
|
||||||
|
exec('docker compose down', () => resolve())
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,60 +1,151 @@
|
|||||||
import {
|
import {
|
||||||
AssetFileUploadResponseDto,
|
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
CreateAssetDto,
|
CreateAssetDto,
|
||||||
CreateLibraryDto,
|
|
||||||
CreateUserDto,
|
CreateUserDto,
|
||||||
PersonCreateDto,
|
PersonUpdateDto,
|
||||||
SharedLinkCreateDto,
|
SharedLinkCreateDto,
|
||||||
ValidateLibraryDto,
|
|
||||||
createAlbum,
|
createAlbum,
|
||||||
createApiKey,
|
createApiKey,
|
||||||
createLibrary,
|
|
||||||
createPerson,
|
createPerson,
|
||||||
createSharedLink,
|
createSharedLink,
|
||||||
createUser,
|
createUser,
|
||||||
defaults,
|
defaults,
|
||||||
deleteAssets,
|
|
||||||
getAssetInfo,
|
|
||||||
login,
|
login,
|
||||||
setAdminOnboarding,
|
setAdminOnboarding,
|
||||||
signUpAdmin,
|
signUpAdmin,
|
||||||
validate,
|
updatePerson,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { BrowserContext } from '@playwright/test';
|
import { BrowserContext } from '@playwright/test';
|
||||||
import { exec, spawn } from 'node:child_process';
|
import { exec, spawn } from 'child_process';
|
||||||
import { createHash } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { existsSync } from 'node:fs';
|
import { access } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
import { io, type Socket } from 'socket.io-client';
|
|
||||||
import { loginDto, signupDto } from 'src/fixtures';
|
import { loginDto, signupDto } from 'src/fixtures';
|
||||||
import { makeRandomImage } from 'src/generators';
|
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
type CliResponse = { stdout: string; stderr: string; exitCode: number | null };
|
const execPromise = promisify(exec);
|
||||||
type EventType = 'assetUpload' | 'assetDelete' | 'userDelete';
|
|
||||||
type WaitOptions = { event: EventType; id: string; timeout?: number };
|
|
||||||
type AdminSetupOptions = { onboarding?: boolean };
|
|
||||||
type AssetData = { bytes?: Buffer; filename: string };
|
|
||||||
|
|
||||||
const dbUrl = 'postgres://postgres:postgres@127.0.0.1:5433/immich';
|
export const app = 'http://127.0.0.1:2283/api';
|
||||||
const baseUrl = 'http://127.0.0.1:2283';
|
|
||||||
|
const directoryExists = (directory: string) =>
|
||||||
|
access(directory)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
export const app = `${baseUrl}/api`;
|
|
||||||
// TODO move test assets into e2e/assets
|
// TODO move test assets into e2e/assets
|
||||||
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
||||||
export const testAssetDirInternal = '/data/assets';
|
|
||||||
export const tempDir = tmpdir();
|
const serverContainerName = 'immich-e2e-server';
|
||||||
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
const uploadMediaDir = '/usr/src/app/upload/upload';
|
||||||
|
|
||||||
|
if (!(await directoryExists(`${testAssetDir}/albums`))) {
|
||||||
|
throw new Error(
|
||||||
|
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setBaseUrl = () => (defaults.baseUrl = app);
|
||||||
|
export const asBearerAuth = (accessToken: string) => ({
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
});
|
||||||
|
|
||||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||||
|
|
||||||
|
let client: pg.Client | null = null;
|
||||||
|
|
||||||
|
export const fileUtils = {
|
||||||
|
reset: async () => {
|
||||||
|
await execPromise(
|
||||||
|
`docker exec -i "${serverContainerName}" rm -R "${uploadMediaDir}"`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dbUtils = {
|
||||||
|
createFace: async ({
|
||||||
|
assetId,
|
||||||
|
personId,
|
||||||
|
}: {
|
||||||
|
assetId: string;
|
||||||
|
personId: string;
|
||||||
|
}) => {
|
||||||
|
if (!client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vector = Array.from({ length: 512 }, Math.random);
|
||||||
|
const embedding = `[${vector.join(',')}]`;
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
|
||||||
|
[assetId, personId, embedding]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setPersonThumbnail: async (personId: string) => {
|
||||||
|
if (!client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
|
||||||
|
[personId]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
reset: async (tables?: string[]) => {
|
||||||
|
try {
|
||||||
|
if (!client) {
|
||||||
|
client = new pg.Client(
|
||||||
|
'postgres://postgres:postgres@127.0.0.1:5433/immich'
|
||||||
|
);
|
||||||
|
await client.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
tables = tables || [
|
||||||
|
'shared_links',
|
||||||
|
'person',
|
||||||
|
'albums',
|
||||||
|
'assets',
|
||||||
|
'asset_faces',
|
||||||
|
'activity',
|
||||||
|
'api_keys',
|
||||||
|
'user_token',
|
||||||
|
'users',
|
||||||
|
'system_metadata',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const table of tables) {
|
||||||
|
await client.query(`DELETE FROM ${table} CASCADE;`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to reset database', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
teardown: async () => {
|
||||||
|
try {
|
||||||
|
if (client) {
|
||||||
|
await client.end();
|
||||||
|
client = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to teardown database', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export interface CliResponse {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
exitCode: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const immichCli = async (args: string[]) => {
|
export const immichCli = async (args: string[]) => {
|
||||||
let _resolve: (value: CliResponse) => void;
|
let _resolve: (value: CliResponse) => void;
|
||||||
const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve));
|
const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve));
|
||||||
const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args];
|
const _args = ['node_modules/.bin/immich', '-d', '/tmp/immich/', ...args];
|
||||||
const child = spawn('node', _args, {
|
const child = spawn('node', _args, {
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
});
|
});
|
||||||
@@ -75,136 +166,14 @@ export const immichCli = async (args: string[]) => {
|
|||||||
return deferred;
|
return deferred;
|
||||||
};
|
};
|
||||||
|
|
||||||
let client: pg.Client | null = null;
|
export interface AdminSetupOptions {
|
||||||
|
onboarding?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const events: Record<EventType, Set<string>> = {
|
export const apiUtils = {
|
||||||
assetUpload: new Set<string>(),
|
setup: () => {
|
||||||
assetDelete: new Set<string>(),
|
setBaseUrl();
|
||||||
userDelete: new Set<string>(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const callbacks: Record<string, () => void> = {};
|
|
||||||
|
|
||||||
const execPromise = promisify(exec);
|
|
||||||
|
|
||||||
const onEvent = ({ event, id }: { event: EventType; id: string }) => {
|
|
||||||
events[event].add(id);
|
|
||||||
const callback = callbacks[id];
|
|
||||||
if (callback) {
|
|
||||||
callback();
|
|
||||||
delete callbacks[id];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const utils = {
|
|
||||||
resetDatabase: async (tables?: string[]) => {
|
|
||||||
try {
|
|
||||||
if (!client) {
|
|
||||||
client = new pg.Client(dbUrl);
|
|
||||||
await client.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
tables = tables || [
|
|
||||||
// TODO e2e test for deleting a stack, since it is quite complex
|
|
||||||
'asset_stack',
|
|
||||||
'libraries',
|
|
||||||
'shared_links',
|
|
||||||
'person',
|
|
||||||
'albums',
|
|
||||||
'assets',
|
|
||||||
'asset_faces',
|
|
||||||
'activity',
|
|
||||||
'api_keys',
|
|
||||||
'user_token',
|
|
||||||
'users',
|
|
||||||
'system_metadata',
|
|
||||||
];
|
|
||||||
|
|
||||||
const sql: string[] = [];
|
|
||||||
|
|
||||||
if (tables.includes('asset_stack')) {
|
|
||||||
sql.push('UPDATE "assets" SET "stackId" = NULL;');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const table of tables) {
|
|
||||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.query(sql.join('\n'));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to reset database', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
resetFilesystem: async () => {
|
|
||||||
const mediaInternal = '/usr/src/app/upload';
|
|
||||||
const dirs = [
|
|
||||||
`"${mediaInternal}/thumbs"`,
|
|
||||||
`"${mediaInternal}/upload"`,
|
|
||||||
`"${mediaInternal}/library"`,
|
|
||||||
`"${mediaInternal}/encoded-video"`,
|
|
||||||
].join(' ');
|
|
||||||
|
|
||||||
await execPromise(`docker exec -i "immich-e2e-server" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`);
|
|
||||||
},
|
|
||||||
|
|
||||||
unzip: async (input: string, output: string) => {
|
|
||||||
await execPromise(`unzip -o -d "${output}" "${input}"`);
|
|
||||||
},
|
|
||||||
|
|
||||||
sha1: (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64'),
|
|
||||||
|
|
||||||
connectWebsocket: async (accessToken: string) => {
|
|
||||||
const websocket = io(baseUrl, {
|
|
||||||
path: '/api/socket.io',
|
|
||||||
transports: ['websocket'],
|
|
||||||
extraHeaders: { Authorization: `Bearer ${accessToken}` },
|
|
||||||
autoConnect: true,
|
|
||||||
forceNew: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise<Socket>((resolve) => {
|
|
||||||
websocket
|
|
||||||
.on('connect', () => resolve(websocket))
|
|
||||||
.on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'assetUpload', id: data.id }))
|
|
||||||
.on('on_asset_delete', (assetId: string) => onEvent({ event: 'assetDelete', id: assetId }))
|
|
||||||
.on('on_user_delete', (userId: string) => onEvent({ event: 'userDelete', id: userId }))
|
|
||||||
.connect();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
disconnectWebsocket: (ws: Socket) => {
|
|
||||||
if (ws?.connected) {
|
|
||||||
ws.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const set of Object.values(events)) {
|
|
||||||
set.clear();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
waitForWebsocketEvent: async ({ event, id, timeout: ms }: WaitOptions): Promise<void> => {
|
|
||||||
console.log(`Waiting for ${event} [${id}]`);
|
|
||||||
const set = events[event];
|
|
||||||
if (set.has(id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000);
|
|
||||||
|
|
||||||
callbacks[id] = () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setApiEndpoint: () => {
|
|
||||||
defaults.baseUrl = app;
|
|
||||||
},
|
|
||||||
|
|
||||||
adminSetup: async (options?: AdminSetupOptions) => {
|
adminSetup: async (options?: AdminSetupOptions) => {
|
||||||
options = options || { onboarding: true };
|
options = options || { onboarding: true };
|
||||||
|
|
||||||
@@ -215,98 +184,73 @@ export const utils = {
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
||||||
await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
|
await createUser(
|
||||||
|
{ createUserDto: dto },
|
||||||
|
{ headers: asBearerAuth(accessToken) }
|
||||||
|
);
|
||||||
return login({
|
return login({
|
||||||
loginCredentialDto: { email: dto.email, password: dto.password },
|
loginCredentialDto: { email: dto.email, password: dto.password },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
createApiKey: (accessToken: string) => {
|
createApiKey: (accessToken: string) => {
|
||||||
return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) });
|
return createApiKey(
|
||||||
|
{ apiKeyCreateDto: { name: 'e2e' } },
|
||||||
|
{ headers: asBearerAuth(accessToken) }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
||||||
createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }),
|
createAlbum(
|
||||||
|
{ createAlbumDto: dto },
|
||||||
|
{ headers: asBearerAuth(accessToken) }
|
||||||
|
),
|
||||||
createAsset: async (
|
createAsset: async (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
dto?: Partial<Omit<CreateAssetDto, 'assetData'>> & { assetData?: AssetData },
|
dto?: Omit<CreateAssetDto, 'assetData'>
|
||||||
) => {
|
) => {
|
||||||
const _dto = {
|
dto = dto || {
|
||||||
deviceAssetId: 'test-1',
|
deviceAssetId: 'test-1',
|
||||||
deviceId: 'test',
|
deviceId: 'test',
|
||||||
fileCreatedAt: new Date().toISOString(),
|
fileCreatedAt: new Date().toISOString(),
|
||||||
fileModifiedAt: new Date().toISOString(),
|
fileModifiedAt: new Date().toISOString(),
|
||||||
...dto,
|
|
||||||
};
|
};
|
||||||
|
const { body } = await request(app)
|
||||||
const assetData = dto?.assetData?.bytes || makeRandomImage();
|
|
||||||
const filename = dto?.assetData?.filename || 'example.png';
|
|
||||||
|
|
||||||
if (dto?.assetData?.bytes) {
|
|
||||||
console.log(`Uploading ${filename}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const builder = request(app)
|
|
||||||
.post(`/asset/upload`)
|
.post(`/asset/upload`)
|
||||||
.attach('assetData', assetData, filename)
|
.field('deviceAssetId', dto.deviceAssetId)
|
||||||
|
.field('deviceId', dto.deviceId)
|
||||||
|
.field('fileCreatedAt', dto.fileCreatedAt)
|
||||||
|
.field('fileModifiedAt', dto.fileModifiedAt)
|
||||||
|
.attach('assetData', randomBytes(32), 'example.jpg')
|
||||||
.set('Authorization', `Bearer ${accessToken}`);
|
.set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(_dto)) {
|
return body as AssetResponseDto;
|
||||||
void builder.field(key, String(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
const { body } = await builder;
|
|
||||||
|
|
||||||
return body as AssetFileUploadResponseDto;
|
|
||||||
},
|
},
|
||||||
|
createPerson: async (accessToken: string, dto: PersonUpdateDto) => {
|
||||||
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
// TODO fix createPerson to accept a body
|
||||||
|
const { id } = await createPerson({ headers: asBearerAuth(accessToken) });
|
||||||
deleteAssets: (accessToken: string, ids: string[]) =>
|
await dbUtils.setPersonThumbnail(id);
|
||||||
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
|
return updatePerson(
|
||||||
|
{ id, personUpdateDto: dto },
|
||||||
createPerson: async (accessToken: string, dto?: PersonCreateDto) => {
|
{ headers: asBearerAuth(accessToken) }
|
||||||
const person = await createPerson({ personCreateDto: dto || {} }, { headers: asBearerAuth(accessToken) });
|
);
|
||||||
await utils.setPersonThumbnail(person.id);
|
|
||||||
|
|
||||||
return person;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => {
|
|
||||||
if (!client) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vector = Array.from({ length: 512 }, Math.random);
|
|
||||||
const embedding = `[${vector.join(',')}]`;
|
|
||||||
|
|
||||||
await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [
|
|
||||||
assetId,
|
|
||||||
personId,
|
|
||||||
embedding,
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
setPersonThumbnail: async (personId: string) => {
|
|
||||||
if (!client) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]);
|
|
||||||
},
|
|
||||||
|
|
||||||
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
|
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
|
||||||
createSharedLink({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }),
|
createSharedLink(
|
||||||
|
{ sharedLinkCreateDto: dto },
|
||||||
|
{ headers: asBearerAuth(accessToken) }
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
createLibrary: (accessToken: string, dto: CreateLibraryDto) =>
|
export const cliUtils = {
|
||||||
createLibrary({ createLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
|
login: async () => {
|
||||||
|
const admin = await apiUtils.adminSetup();
|
||||||
validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) =>
|
const key = await apiUtils.createApiKey(admin.accessToken);
|
||||||
validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
|
await immichCli(['login-key', app, `${key.secret}`]);
|
||||||
|
return key.secret;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const webUtils = {
|
||||||
setAuthCookies: async (context: BrowserContext, accessToken: string) =>
|
setAuthCookies: async (context: BrowserContext, accessToken: string) =>
|
||||||
await context.addCookies([
|
await context.addCookies([
|
||||||
{
|
{
|
||||||
@@ -314,7 +258,7 @@ export const utils = {
|
|||||||
value: accessToken,
|
value: accessToken,
|
||||||
domain: '127.0.0.1',
|
domain: '127.0.0.1',
|
||||||
path: '/',
|
path: '/',
|
||||||
expires: 1_742_402_728,
|
expires: 1742402728,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: 'Lax',
|
sameSite: 'Lax',
|
||||||
@@ -324,7 +268,7 @@ export const utils = {
|
|||||||
value: 'password',
|
value: 'password',
|
||||||
domain: '127.0.0.1',
|
domain: '127.0.0.1',
|
||||||
path: '/',
|
path: '/',
|
||||||
expires: 1_742_402_728,
|
expires: 1742402728,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: 'Lax',
|
sameSite: 'Lax',
|
||||||
@@ -334,25 +278,10 @@ export const utils = {
|
|||||||
value: 'true',
|
value: 'true',
|
||||||
domain: '127.0.0.1',
|
domain: '127.0.0.1',
|
||||||
path: '/',
|
path: '/',
|
||||||
expires: 1_742_402_728,
|
expires: 1742402728,
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: 'Lax',
|
sameSite: 'Lax',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
|
||||||
cliLogin: async () => {
|
|
||||||
const admin = await utils.adminSetup();
|
|
||||||
const key = await utils.createApiKey(admin.accessToken);
|
|
||||||
await immichCli(['login-key', app, `${key.secret}`]);
|
|
||||||
return key.secret;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
utils.setApiEndpoint();
|
|
||||||
|
|
||||||
if (!existsSync(`${testAssetDir}/albums`)) {
|
|
||||||
throw new Error(
|
|
||||||
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { utils } from 'src/utils';
|
import { apiUtils, dbUtils, webUtils } from 'src/utils';
|
||||||
|
|
||||||
test.describe('Registration', () => {
|
test.describe('Registration', () => {
|
||||||
test.beforeAll(() => {
|
test.beforeAll(() => {
|
||||||
utils.setApiEndpoint();
|
apiUtils.setup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await utils.resetDatabase();
|
await dbUtils.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await dbUtils.teardown();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('admin registration', async ({ page }) => {
|
test('admin registration', async ({ page }) => {
|
||||||
@@ -41,8 +45,8 @@ test.describe('Registration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('user registration', async ({ context, page }) => {
|
test('user registration', async ({ context, page }) => {
|
||||||
const admin = await utils.adminSetup();
|
const admin = await apiUtils.adminSetup();
|
||||||
await utils.setAuthCookies(context, admin.accessToken);
|
await webUtils.setAuthCookies(context, admin.accessToken);
|
||||||
|
|
||||||
// create user
|
// create user
|
||||||
await page.goto('/admin/user-management');
|
await page.goto('/admin/user-management');
|
||||||
@@ -64,7 +68,7 @@ test.describe('Registration', () => {
|
|||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
|
|
||||||
// change password
|
// change password
|
||||||
await expect(page.getByRole('heading')).toHaveText('Change Password');
|
expect(page.getByRole('heading')).toHaveText('Change Password');
|
||||||
await expect(page).toHaveURL('/auth/change-password');
|
await expect(page).toHaveURL('/auth/change-password');
|
||||||
await page.getByLabel('New Password').fill('new-password');
|
await page.getByLabel('New Password').fill('new-password');
|
||||||
await page.getByLabel('Confirm Password').fill('new-password');
|
await page.getByLabel('Confirm Password').fill('new-password');
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import {
|
import {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AssetFileUploadResponseDto,
|
AssetResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
|
createSharedLink,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { test } from '@playwright/test';
|
import { test } from '@playwright/test';
|
||||||
import { asBearerAuth, utils } from 'src/utils';
|
import { apiUtils, asBearerAuth, dbUtils } from 'src/utils';
|
||||||
|
|
||||||
test.describe('Shared Links', () => {
|
test.describe('Shared Links', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let asset: AssetFileUploadResponseDto;
|
let asset: AssetResponseDto;
|
||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
let sharedLink: SharedLinkResponseDto;
|
let sharedLink: SharedLinkResponseDto;
|
||||||
let sharedLinkPassword: SharedLinkResponseDto;
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
utils.setApiEndpoint();
|
apiUtils.setup();
|
||||||
await utils.resetDatabase();
|
await dbUtils.reset();
|
||||||
admin = await utils.adminSetup();
|
admin = await apiUtils.adminSetup();
|
||||||
asset = await utils.createAsset(admin.accessToken);
|
asset = await apiUtils.createAsset(admin.accessToken);
|
||||||
album = await createAlbum(
|
album = await createAlbum(
|
||||||
{
|
{
|
||||||
createAlbumDto: {
|
createAlbumDto: {
|
||||||
@@ -28,17 +28,22 @@ test.describe('Shared Links', () => {
|
|||||||
assetIds: [asset.id],
|
assetIds: [asset.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
|
// { headers: asBearerAuth(admin.accessToken)},
|
||||||
);
|
);
|
||||||
sharedLink = await utils.createSharedLink(admin.accessToken, {
|
sharedLink = await createSharedLink(
|
||||||
type: SharedLinkType.Album,
|
{
|
||||||
albumId: album.id,
|
sharedLinkCreateDto: {
|
||||||
});
|
type: SharedLinkType.Album,
|
||||||
sharedLinkPassword = await utils.createSharedLink(admin.accessToken, {
|
albumId: album.id,
|
||||||
type: SharedLinkType.Album,
|
},
|
||||||
albumId: album.id,
|
},
|
||||||
password: 'test-password',
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
});
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await dbUtils.teardown();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('download from a shared link', async ({ page }) => {
|
test('download from a shared link', async ({ page }) => {
|
||||||
@@ -48,18 +53,6 @@ test.describe('Shared Links', () => {
|
|||||||
await page.waitForSelector('#asset-group-by-date svg');
|
await page.waitForSelector('#asset-group-by-date svg');
|
||||||
await page.getByRole('checkbox').click();
|
await page.getByRole('checkbox').click();
|
||||||
await page.getByRole('button', { name: 'Download' }).click();
|
await page.getByRole('button', { name: 'Download' }).click();
|
||||||
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
|
await page.getByText('DOWNLOADING').waitFor();
|
||||||
});
|
|
||||||
|
|
||||||
test('enter password for a shared link', async ({ page }) => {
|
|
||||||
await page.goto(`/share/${sharedLinkPassword.key}`);
|
|
||||||
await page.getByPlaceholder('Password').fill('test-password');
|
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
|
||||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('show error for invalid shared link', async ({ page }) => {
|
|
||||||
await page.goto('/share/invalid');
|
|
||||||
await page.getByRole('heading', { name: 'Invalid share key' }).waitFor();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,5 @@
|
|||||||
"rootDirs": ["src"],
|
"rootDirs": ["src"],
|
||||||
"baseUrl": "./"
|
"baseUrl": "./"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,9 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
// skip `docker compose up` if `make e2e` was already run
|
|
||||||
const globalSetup: string[] = [];
|
|
||||||
try {
|
|
||||||
await fetch('http://127.0.0.1:2283/api/server-info/ping');
|
|
||||||
} catch {
|
|
||||||
globalSetup.push('src/setup.ts');
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
include: ['src/{api,cli}/specs/*.e2e-spec.ts'],
|
include: ['src/{api,cli}/specs/*.e2e-spec.ts'],
|
||||||
globalSetup,
|
globalSetup: ['src/setup.ts'],
|
||||||
testTimeout: 10_000,
|
|
||||||
poolOptions: {
|
poolOptions: {
|
||||||
threads: {
|
threads: {
|
||||||
singleThread: true,
|
singleThread: true,
|
||||||
|
|||||||
@@ -167,10 +167,6 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# VS Code
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
*.onnx
|
*.onnx
|
||||||
*.zip
|
*.zip
|
||||||
|
|
||||||
core
|
|
||||||
@@ -39,7 +39,7 @@ FROM python:3.11-slim-bookworm@sha256:ce81dc539f0aedc9114cae640f8352fad83d37461c
|
|||||||
FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as prod-openvino
|
FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as prod-openvino
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04@sha256:2d913b09e6be8387e1a10976933642c73c840c0b735f0bf3c28d97fc9bc422e0 as prod-cuda
|
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04@sha256:85fb7ac694079fff1061a0140fd5b5a641997880e12112d92589c3bbb1e8b7ca as prod-cuda
|
||||||
|
|
||||||
COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3
|
COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3
|
||||||
COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11
|
COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
from socket import socket
|
from socket import socket
|
||||||
|
|
||||||
from gunicorn.arbiter import Arbiter
|
from gunicorn.arbiter import Arbiter
|
||||||
from pydantic import BaseModel, BaseSettings
|
from pydantic import BaseSettings
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
from uvicorn import Server
|
from uvicorn import Server
|
||||||
@@ -15,11 +15,6 @@ from uvicorn.workers import UvicornWorker
|
|||||||
from .schemas import ModelType
|
from .schemas import ModelType
|
||||||
|
|
||||||
|
|
||||||
class PreloadModelData(BaseModel):
|
|
||||||
clip: str | None
|
|
||||||
facial_recognition: str | None
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
cache_folder: str = "/cache"
|
cache_folder: str = "/cache"
|
||||||
model_ttl: int = 300
|
model_ttl: int = 300
|
||||||
@@ -32,12 +27,10 @@ class Settings(BaseSettings):
|
|||||||
model_inter_op_threads: int = 0
|
model_inter_op_threads: int = 0
|
||||||
model_intra_op_threads: int = 0
|
model_intra_op_threads: int = 0
|
||||||
ann: bool = True
|
ann: bool = True
|
||||||
preload: PreloadModelData | None = None
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_prefix = "MACHINE_LEARNING_"
|
env_prefix = "MACHINE_LEARNING_"
|
||||||
case_sensitive = False
|
case_sensitive = False
|
||||||
env_nested_delimiter = "__"
|
|
||||||
|
|
||||||
|
|
||||||
class LogSettings(BaseSettings):
|
class LogSettings(BaseSettings):
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from starlette.formparsers import MultiPartParser
|
|||||||
|
|
||||||
from app.models.base import InferenceModel
|
from app.models.base import InferenceModel
|
||||||
|
|
||||||
from .config import PreloadModelData, log, settings
|
from .config import log, settings
|
||||||
from .models.cache import ModelCache
|
from .models.cache import ModelCache
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
MessageResponse,
|
MessageResponse,
|
||||||
@@ -27,7 +27,7 @@ from .schemas import (
|
|||||||
|
|
||||||
MultiPartParser.max_file_size = 2**26 # spools to disk if payload is 64 MiB or larger
|
MultiPartParser.max_file_size = 2**26 # spools to disk if payload is 64 MiB or larger
|
||||||
|
|
||||||
model_cache = ModelCache(revalidate=settings.model_ttl > 0)
|
model_cache = ModelCache(ttl=settings.model_ttl, revalidate=settings.model_ttl > 0)
|
||||||
thread_pool: ThreadPoolExecutor | None = None
|
thread_pool: ThreadPoolExecutor | None = None
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
active_requests = 0
|
active_requests = 0
|
||||||
@@ -51,8 +51,6 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
log.info(f"Initialized request thread pool with {settings.request_threads} threads.")
|
log.info(f"Initialized request thread pool with {settings.request_threads} threads.")
|
||||||
if settings.model_ttl > 0 and settings.model_ttl_poll_s > 0:
|
if settings.model_ttl > 0 and settings.model_ttl_poll_s > 0:
|
||||||
asyncio.ensure_future(idle_shutdown_task())
|
asyncio.ensure_future(idle_shutdown_task())
|
||||||
if settings.preload is not None:
|
|
||||||
await preload_models(settings.preload)
|
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
log.handlers.clear()
|
log.handlers.clear()
|
||||||
@@ -63,14 +61,6 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
|
|
||||||
async def preload_models(preload_models: PreloadModelData) -> None:
|
|
||||||
log.info(f"Preloading models: {preload_models}")
|
|
||||||
if preload_models.clip is not None:
|
|
||||||
await load(await model_cache.get(preload_models.clip, ModelType.CLIP))
|
|
||||||
if preload_models.facial_recognition is not None:
|
|
||||||
await load(await model_cache.get(preload_models.facial_recognition, ModelType.FACIAL_RECOGNITION))
|
|
||||||
|
|
||||||
|
|
||||||
def update_state() -> Iterator[None]:
|
def update_state() -> Iterator[None]:
|
||||||
global active_requests, last_called
|
global active_requests, last_called
|
||||||
active_requests += 1
|
active_requests += 1
|
||||||
@@ -113,7 +103,7 @@ async def predict(
|
|||||||
except orjson.JSONDecodeError:
|
except orjson.JSONDecodeError:
|
||||||
raise HTTPException(400, f"Invalid options JSON: {options}")
|
raise HTTPException(400, f"Invalid options JSON: {options}")
|
||||||
|
|
||||||
model = await load(await model_cache.get(model_name, model_type, ttl=settings.model_ttl, **kwargs))
|
model = await load(await model_cache.get(model_name, model_type, **kwargs))
|
||||||
model.configure(**kwargs)
|
model.configure(**kwargs)
|
||||||
outputs = await run(model.predict, inputs)
|
outputs = await run(model.predict, inputs)
|
||||||
return ORJSONResponse(outputs)
|
return ORJSONResponse(outputs)
|
||||||
|
|||||||
@@ -165,14 +165,6 @@ class InferenceModel(ABC):
|
|||||||
def providers_default(self) -> list[str]:
|
def providers_default(self) -> list[str]:
|
||||||
available_providers = set(ort.get_available_providers())
|
available_providers = set(ort.get_available_providers())
|
||||||
log.debug(f"Available ORT providers: {available_providers}")
|
log.debug(f"Available ORT providers: {available_providers}")
|
||||||
if (openvino := "OpenVINOExecutionProvider") in available_providers:
|
|
||||||
device_ids: list[str] = ort.capi._pybind_state.get_available_openvino_device_ids()
|
|
||||||
log.debug(f"Available OpenVINO devices: {device_ids}")
|
|
||||||
|
|
||||||
gpu_devices = [device_id for device_id in device_ids if device_id.startswith("GPU")]
|
|
||||||
if not gpu_devices:
|
|
||||||
log.warning("No GPU device found in OpenVINO. Falling back to CPU.")
|
|
||||||
available_providers.remove(openvino)
|
|
||||||
return [provider for provider in SUPPORTED_PROVIDERS if provider in available_providers]
|
return [provider for provider in SUPPORTED_PROVIDERS if provider in available_providers]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -192,7 +184,15 @@ class InferenceModel(ABC):
|
|||||||
case "CPUExecutionProvider" | "CUDAExecutionProvider":
|
case "CPUExecutionProvider" | "CUDAExecutionProvider":
|
||||||
option = {"arena_extend_strategy": "kSameAsRequested"}
|
option = {"arena_extend_strategy": "kSameAsRequested"}
|
||||||
case "OpenVINOExecutionProvider":
|
case "OpenVINOExecutionProvider":
|
||||||
option = {"device_type": "GPU_FP32"}
|
try:
|
||||||
|
device_ids: list[str] = ort.capi._pybind_state.get_available_openvino_device_ids()
|
||||||
|
log.debug(f"Available OpenVINO devices: {device_ids}")
|
||||||
|
gpu_devices = [device_id for device_id in device_ids if device_id.startswith("GPU")]
|
||||||
|
option = {"device_id": gpu_devices[0]} if gpu_devices else {}
|
||||||
|
except AttributeError as e:
|
||||||
|
log.warning("Failed to get OpenVINO device IDs. Using default options.")
|
||||||
|
log.error(e)
|
||||||
|
option = {}
|
||||||
case _:
|
case _:
|
||||||
option = {}
|
option = {}
|
||||||
options.append(option)
|
options.append(option)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from typing import Any
|
|||||||
|
|
||||||
from aiocache.backends.memory import SimpleMemoryCache
|
from aiocache.backends.memory import SimpleMemoryCache
|
||||||
from aiocache.lock import OptimisticLock
|
from aiocache.lock import OptimisticLock
|
||||||
from aiocache.plugins import TimingPlugin
|
from aiocache.plugins import BasePlugin, TimingPlugin
|
||||||
|
|
||||||
from app.models import from_model_type
|
from app.models import from_model_type
|
||||||
|
|
||||||
@@ -15,25 +15,28 @@ class ModelCache:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
ttl: float | None = None,
|
||||||
revalidate: bool = False,
|
revalidate: bool = False,
|
||||||
timeout: int | None = None,
|
timeout: int | None = None,
|
||||||
profiling: bool = False,
|
profiling: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
|
ttl: Unloads model after this duration. Disabled if None. Defaults to None.
|
||||||
revalidate: Resets TTL on cache hit. Useful to keep models in memory while active. Defaults to False.
|
revalidate: Resets TTL on cache hit. Useful to keep models in memory while active. Defaults to False.
|
||||||
timeout: Maximum allowed time for model to load. Disabled if None. Defaults to None.
|
timeout: Maximum allowed time for model to load. Disabled if None. Defaults to None.
|
||||||
profiling: Collects metrics for cache operations, adding slight overhead. Defaults to False.
|
profiling: Collects metrics for cache operations, adding slight overhead. Defaults to False.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
self.ttl = ttl
|
||||||
plugins = []
|
plugins = []
|
||||||
|
|
||||||
|
if revalidate:
|
||||||
|
plugins.append(RevalidationPlugin())
|
||||||
if profiling:
|
if profiling:
|
||||||
plugins.append(TimingPlugin())
|
plugins.append(TimingPlugin())
|
||||||
|
|
||||||
self.revalidate_enable = revalidate
|
self.cache = SimpleMemoryCache(ttl=ttl, timeout=timeout, plugins=plugins, namespace=None)
|
||||||
|
|
||||||
self.cache = SimpleMemoryCache(timeout=timeout, plugins=plugins, namespace=None)
|
|
||||||
|
|
||||||
async def get(self, model_name: str, model_type: ModelType, **model_kwargs: Any) -> InferenceModel:
|
async def get(self, model_name: str, model_type: ModelType, **model_kwargs: Any) -> InferenceModel:
|
||||||
"""
|
"""
|
||||||
@@ -46,14 +49,11 @@ class ModelCache:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
key = f"{model_name}{model_type.value}{model_kwargs.get('mode', '')}"
|
key = f"{model_name}{model_type.value}{model_kwargs.get('mode', '')}"
|
||||||
|
|
||||||
async with OptimisticLock(self.cache, key) as lock:
|
async with OptimisticLock(self.cache, key) as lock:
|
||||||
model: InferenceModel | None = await self.cache.get(key)
|
model: InferenceModel | None = await self.cache.get(key)
|
||||||
if model is None:
|
if model is None:
|
||||||
model = from_model_type(model_type, model_name, **model_kwargs)
|
model = from_model_type(model_type, model_name, **model_kwargs)
|
||||||
await lock.cas(model, ttl=model_kwargs.get("ttl", None))
|
await lock.cas(model, ttl=self.ttl)
|
||||||
elif self.revalidate_enable:
|
|
||||||
await self.revalidate(key, model_kwargs.get("ttl", None))
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
async def get_profiling(self) -> dict[str, float] | None:
|
async def get_profiling(self) -> dict[str, float] | None:
|
||||||
@@ -62,6 +62,21 @@ class ModelCache:
|
|||||||
|
|
||||||
return self.cache.profiling
|
return self.cache.profiling
|
||||||
|
|
||||||
async def revalidate(self, key: str, ttl: int | None) -> None:
|
|
||||||
if ttl is not None and key in self.cache._handlers:
|
class RevalidationPlugin(BasePlugin): # type: ignore[misc]
|
||||||
await self.cache.expire(key, ttl)
|
"""Revalidates cache item's TTL after cache hit."""
|
||||||
|
|
||||||
|
async def post_get(
|
||||||
|
self,
|
||||||
|
client: SimpleMemoryCache,
|
||||||
|
key: str,
|
||||||
|
ret: Any | None = None,
|
||||||
|
namespace: str | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
if ret is None:
|
||||||
|
return
|
||||||
|
if namespace is not None:
|
||||||
|
key = client.build_key(key, namespace)
|
||||||
|
if key in client._handlers:
|
||||||
|
await client.expire(key, client.ttl)
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ import onnxruntime as ort
|
|||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from pytest import MonkeyPatch
|
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from app.main import load, preload_models
|
from app.main import load
|
||||||
|
|
||||||
from .config import Settings, log, settings
|
from .config import log, settings
|
||||||
from .models.base import InferenceModel
|
from .models.base import InferenceModel
|
||||||
from .models.cache import ModelCache
|
from .models.cache import ModelCache
|
||||||
from .models.clip import MCLIPEncoder, OpenCLIPEncoder
|
from .models.clip import MCLIPEncoder, OpenCLIPEncoder
|
||||||
@@ -46,23 +45,11 @@ class TestBase:
|
|||||||
assert encoder.providers == self.CUDA_EP
|
assert encoder.providers == self.CUDA_EP
|
||||||
|
|
||||||
@pytest.mark.providers(OV_EP)
|
@pytest.mark.providers(OV_EP)
|
||||||
def test_sets_openvino_provider_if_available(self, providers: list[str], mocker: MockerFixture) -> None:
|
def test_sets_openvino_provider_if_available(self, providers: list[str]) -> None:
|
||||||
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
|
|
||||||
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
|
|
||||||
|
|
||||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||||
|
|
||||||
assert encoder.providers == self.OV_EP
|
assert encoder.providers == self.OV_EP
|
||||||
|
|
||||||
@pytest.mark.providers(OV_EP)
|
|
||||||
def test_avoids_openvino_if_gpu_not_available(self, providers: list[str], mocker: MockerFixture) -> None:
|
|
||||||
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
|
|
||||||
mocked.get_available_openvino_device_ids.return_value = ["CPU"]
|
|
||||||
|
|
||||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
|
||||||
|
|
||||||
assert encoder.providers == self.CPU_EP
|
|
||||||
|
|
||||||
@pytest.mark.providers(CUDA_EP_OUT_OF_ORDER)
|
@pytest.mark.providers(CUDA_EP_OUT_OF_ORDER)
|
||||||
def test_sets_providers_in_correct_order(self, providers: list[str]) -> None:
|
def test_sets_providers_in_correct_order(self, providers: list[str]) -> None:
|
||||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||||
@@ -81,14 +68,22 @@ class TestBase:
|
|||||||
|
|
||||||
assert encoder.providers == providers
|
assert encoder.providers == providers
|
||||||
|
|
||||||
def test_sets_default_provider_options(self, mocker: MockerFixture) -> None:
|
def test_sets_default_provider_options(self) -> None:
|
||||||
|
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
|
||||||
|
|
||||||
|
assert encoder.provider_options == [
|
||||||
|
{},
|
||||||
|
{"arena_extend_strategy": "kSameAsRequested"},
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_sets_openvino_device_id_if_possible(self, mocker: MockerFixture) -> None:
|
||||||
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
|
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
|
||||||
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
|
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
|
||||||
|
|
||||||
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
|
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
|
||||||
|
|
||||||
assert encoder.provider_options == [
|
assert encoder.provider_options == [
|
||||||
{"device_type": "GPU_FP32"},
|
{"device_id": "GPU.0"},
|
||||||
{"arena_extend_strategy": "kSameAsRequested"},
|
{"arena_extend_strategy": "kSameAsRequested"},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -510,20 +505,20 @@ class TestCache:
|
|||||||
|
|
||||||
@mock.patch("app.models.cache.OptimisticLock", autospec=True)
|
@mock.patch("app.models.cache.OptimisticLock", autospec=True)
|
||||||
async def test_model_ttl(self, mock_lock_cls: mock.Mock, mock_get_model: mock.Mock) -> None:
|
async def test_model_ttl(self, mock_lock_cls: mock.Mock, mock_get_model: mock.Mock) -> None:
|
||||||
model_cache = ModelCache()
|
model_cache = ModelCache(ttl=100)
|
||||||
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100)
|
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION)
|
||||||
mock_lock_cls.return_value.__aenter__.return_value.cas.assert_called_with(mock.ANY, ttl=100)
|
mock_lock_cls.return_value.__aenter__.return_value.cas.assert_called_with(mock.ANY, ttl=100)
|
||||||
|
|
||||||
@mock.patch("app.models.cache.SimpleMemoryCache.expire")
|
@mock.patch("app.models.cache.SimpleMemoryCache.expire")
|
||||||
async def test_revalidate_get(self, mock_cache_expire: mock.Mock, mock_get_model: mock.Mock) -> None:
|
async def test_revalidate_get(self, mock_cache_expire: mock.Mock, mock_get_model: mock.Mock) -> None:
|
||||||
model_cache = ModelCache(revalidate=True)
|
model_cache = ModelCache(ttl=100, revalidate=True)
|
||||||
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100)
|
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION)
|
||||||
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100)
|
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION)
|
||||||
mock_cache_expire.assert_called_once_with(mock.ANY, 100)
|
mock_cache_expire.assert_called_once_with(mock.ANY, 100)
|
||||||
|
|
||||||
async def test_profiling(self, mock_get_model: mock.Mock) -> None:
|
async def test_profiling(self, mock_get_model: mock.Mock) -> None:
|
||||||
model_cache = ModelCache(profiling=True)
|
model_cache = ModelCache(ttl=100, profiling=True)
|
||||||
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100)
|
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION)
|
||||||
profiling = await model_cache.get_profiling()
|
profiling = await model_cache.get_profiling()
|
||||||
assert isinstance(profiling, dict)
|
assert isinstance(profiling, dict)
|
||||||
assert profiling == model_cache.cache.profiling
|
assert profiling == model_cache.cache.profiling
|
||||||
@@ -549,25 +544,6 @@ class TestCache:
|
|||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await model_cache.get("test_model_name", ModelType.CLIP, mode="text")
|
await model_cache.get("test_model_name", ModelType.CLIP, mode="text")
|
||||||
|
|
||||||
async def test_preloads_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None:
|
|
||||||
os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai"
|
|
||||||
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s"
|
|
||||||
|
|
||||||
settings = Settings()
|
|
||||||
assert settings.preload is not None
|
|
||||||
assert settings.preload.clip == "ViT-B-32__openai"
|
|
||||||
assert settings.preload.facial_recognition == "buffalo_s"
|
|
||||||
|
|
||||||
model_cache = ModelCache()
|
|
||||||
monkeypatch.setattr("app.main.model_cache", model_cache)
|
|
||||||
|
|
||||||
await preload_models(settings.preload)
|
|
||||||
assert len(model_cache.cache._cache) == 2
|
|
||||||
assert mock_get_model.call_count == 2
|
|
||||||
await model_cache.get("ViT-B-32__openai", ModelType.CLIP, ttl=100)
|
|
||||||
await model_cache.get("buffalo_s", ModelType.FACIAL_RECOGNITION, ttl=100)
|
|
||||||
assert mock_get_model.call_count == 2
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
class TestLoad:
|
class TestLoad:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM mambaorg/micromamba:bookworm-slim@sha256:96586e238e2fed914b839e50cf91943b5655262348d141466b34ced2e0b5b155 as builder
|
FROM mambaorg/micromamba:bookworm-slim@sha256:926cac38640709f90f3fef2a3f730733b5c350be612f0d14706be8833b79ad8c as builder
|
||||||
|
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
TRANSFORMERS_CACHE=/cache \
|
TRANSFORMERS_CACHE=/cache \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiocache"
|
name = "aiocache"
|
||||||
@@ -680,13 +680,13 @@ test = ["pytest (>=6)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.110.0"
|
version = "0.109.2"
|
||||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "fastapi-0.110.0-py3-none-any.whl", hash = "sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b"},
|
{file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"},
|
||||||
{file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"},
|
{file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1250,13 +1250,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpx"
|
name = "httpx"
|
||||||
version = "0.27.0"
|
version = "0.26.0"
|
||||||
description = "The next generation HTTP client."
|
description = "The next generation HTTP client."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
{file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
|
||||||
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
{file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1274,13 +1274,13 @@ socks = ["socksio (==1.*)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "huggingface-hub"
|
name = "huggingface-hub"
|
||||||
version = "0.21.3"
|
version = "0.20.3"
|
||||||
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
|
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
python-versions = ">=3.8.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "huggingface_hub-0.21.3-py3-none-any.whl", hash = "sha256:b183144336fdf2810a8c109822e0bb6ef1fd61c65da6fb60e8c3f658b7144016"},
|
{file = "huggingface_hub-0.20.3-py3-none-any.whl", hash = "sha256:d988ae4f00d3e307b0c80c6a05ca6dbb7edba8bba3079f74cda7d9c2e562a7b6"},
|
||||||
{file = "huggingface_hub-0.21.3.tar.gz", hash = "sha256:26a15b604e4fc7bad37c467b76456543ec849386cbca9cd7e1e135f53e500423"},
|
{file = "huggingface_hub-0.20.3.tar.gz", hash = "sha256:94e7f8e074475fbc67d6a71957b678e1b4a74ff1b64a644fd6cbb83da962d05d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1297,12 +1297,11 @@ all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi",
|
|||||||
cli = ["InquirerPy (==0.3.4)"]
|
cli = ["InquirerPy (==0.3.4)"]
|
||||||
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
|
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
|
||||||
fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
|
fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
|
||||||
hf-transfer = ["hf-transfer (>=0.1.4)"]
|
|
||||||
inference = ["aiohttp", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)"]
|
inference = ["aiohttp", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)"]
|
||||||
quality = ["mypy (==1.5.1)", "ruff (>=0.1.3)"]
|
quality = ["mypy (==1.5.1)", "ruff (>=0.1.3)"]
|
||||||
tensorflow = ["graphviz", "pydot", "tensorflow"]
|
tensorflow = ["graphviz", "pydot", "tensorflow"]
|
||||||
testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
|
testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
|
||||||
torch = ["safetensors", "torch"]
|
torch = ["torch"]
|
||||||
typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"]
|
typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1567,13 +1566,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "locust"
|
name = "locust"
|
||||||
version = "2.24.0"
|
version = "2.23.1"
|
||||||
description = "Developer friendly load testing framework"
|
description = "Developer friendly load testing framework"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "locust-2.24.0-py3-none-any.whl", hash = "sha256:1b6b878b4fd0108fec956120815e69775d2616c8f4d1e9f365c222a7a5c17d9a"},
|
{file = "locust-2.23.1-py3-none-any.whl", hash = "sha256:96013a460a4b4d6d4fd46c70e6ff1fd2b6e03b48ddb1b48d1513d3134ba2cecf"},
|
||||||
{file = "locust-2.24.0.tar.gz", hash = "sha256:6cffa378d995244a7472af6be1d6139331f19aee44e907deee73e0281252804d"},
|
{file = "locust-2.23.1.tar.gz", hash = "sha256:6cc729729e5ebf5852fc9d845302cfcf0ab0132f198e68b3eb0c88b438b6a863"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1589,7 +1588,6 @@ pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
|
|||||||
pyzmq = ">=25.0.0"
|
pyzmq = ">=25.0.0"
|
||||||
requests = ">=2.26.0"
|
requests = ">=2.26.0"
|
||||||
roundrobin = ">=0.0.2"
|
roundrobin = ">=0.0.2"
|
||||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
|
||||||
Werkzeug = ">=2.0.0"
|
Werkzeug = ">=2.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1990,36 +1988,36 @@ reference = ["Pillow", "google-re2"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onnxruntime"
|
name = "onnxruntime"
|
||||||
version = "1.17.1"
|
version = "1.17.0"
|
||||||
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "onnxruntime-1.17.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d43ac17ac4fa3c9096ad3c0e5255bb41fd134560212dc124e7f52c3159af5d21"},
|
{file = "onnxruntime-1.17.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d2b22a25a94109cc983443116da8d9805ced0256eb215c5e6bc6dcbabefeab96"},
|
||||||
{file = "onnxruntime-1.17.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55b5e92a4c76a23981c998078b9bf6145e4fb0b016321a8274b1607bd3c6bd35"},
|
{file = "onnxruntime-1.17.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4c87d83c6f58d1af2675fc99e3dc810f2dbdb844bcefd0c1b7573632661f6fc"},
|
||||||
{file = "onnxruntime-1.17.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebbcd2bc3a066cf54e6f18c75708eb4d309ef42be54606d22e5bdd78afc5b0d7"},
|
{file = "onnxruntime-1.17.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dba55723bf9b835e358f48c98a814b41692c393eb11f51e02ece0625c756b797"},
|
||||||
{file = "onnxruntime-1.17.1-cp310-cp310-win32.whl", hash = "sha256:5e3716b5eec9092e29a8d17aab55e737480487deabfca7eac3cd3ed952b6ada9"},
|
{file = "onnxruntime-1.17.0-cp310-cp310-win32.whl", hash = "sha256:ee48422349cc500273beea7607e33c2237909f58468ae1d6cccfc4aecd158565"},
|
||||||
{file = "onnxruntime-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:fbb98cced6782ae1bb799cc74ddcbbeeae8819f3ad1d942a74d88e72b6511337"},
|
{file = "onnxruntime-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f34cc46553359293854e38bdae2ab1be59543aad78a6317e7746d30e311110c3"},
|
||||||
{file = "onnxruntime-1.17.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:36fd6f87a1ecad87e9c652e42407a50fb305374f9a31d71293eb231caae18784"},
|
{file = "onnxruntime-1.17.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:16d26badd092c8c257fa57c458bb600d96dc15282c647ccad0ed7b2732e6c03b"},
|
||||||
{file = "onnxruntime-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99a8bddeb538edabc524d468edb60ad4722cff8a49d66f4e280c39eace70500b"},
|
{file = "onnxruntime-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f1273bebcdb47ed932d076c85eb9488bc4768fcea16d5f2747ca692fad4f9d3"},
|
||||||
{file = "onnxruntime-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd7fddb4311deb5a7d3390cd8e9b3912d4d963efbe4dfe075edbaf18d01c024e"},
|
{file = "onnxruntime-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb60fd3c2c1acd684752eb9680e89ae223e9801a9b0e0dc7b28adabe45a2e380"},
|
||||||
{file = "onnxruntime-1.17.1-cp311-cp311-win32.whl", hash = "sha256:606a7cbfb6680202b0e4f1890881041ffc3ac6e41760a25763bd9fe146f0b335"},
|
{file = "onnxruntime-1.17.0-cp311-cp311-win32.whl", hash = "sha256:4b038324586bc905299e435f7c00007e6242389c856b82fe9357fdc3b1ef2bdc"},
|
||||||
{file = "onnxruntime-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:53e4e06c0a541696ebdf96085fd9390304b7b04b748a19e02cf3b35c869a1e76"},
|
{file = "onnxruntime-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:93d39b3fa1ee01f034f098e1c7769a811a21365b4883f05f96c14a2b60c6028b"},
|
||||||
{file = "onnxruntime-1.17.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:40f08e378e0f85929712a2b2c9b9a9cc400a90c8a8ca741d1d92c00abec60843"},
|
{file = "onnxruntime-1.17.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:90c0890e36f880281c6c698d9bc3de2afbeee2f76512725ec043665c25c67d21"},
|
||||||
{file = "onnxruntime-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac79da6d3e1bb4590f1dad4bb3c2979d7228555f92bb39820889af8b8e6bd472"},
|
{file = "onnxruntime-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7466724e809a40e986b1637cba156ad9fc0d1952468bc00f79ef340bc0199552"},
|
||||||
{file = "onnxruntime-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ae9ba47dc099004e3781f2d0814ad710a13c868c739ab086fc697524061695ea"},
|
{file = "onnxruntime-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d47bee7557a8b99c8681b6882657a515a4199778d6d5e24e924d2aafcef55b0a"},
|
||||||
{file = "onnxruntime-1.17.1-cp312-cp312-win32.whl", hash = "sha256:2dff1a24354220ac30e4a4ce2fb1df38cb1ea59f7dac2c116238d63fe7f4c5ff"},
|
{file = "onnxruntime-1.17.0-cp312-cp312-win32.whl", hash = "sha256:bb1bf1ee575c665b8bbc3813ab906e091a645a24ccc210be7932154b8260eca1"},
|
||||||
{file = "onnxruntime-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:6226a5201ab8cafb15e12e72ff2a4fc8f50654e8fa5737c6f0bd57c5ff66827e"},
|
{file = "onnxruntime-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac2f286da3494b29b4186ca193c7d4e6a2c1f770c4184c7192c5da142c3dec28"},
|
||||||
{file = "onnxruntime-1.17.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:cd0c07c0d1dfb8629e820b05fda5739e4835b3b82faf43753d2998edf2cf00aa"},
|
{file = "onnxruntime-1.17.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1ec485643b93e0a3896c655eb2426decd63e18a278bb7ccebc133b340723624f"},
|
||||||
{file = "onnxruntime-1.17.1-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:617ebdf49184efa1ba6e4467e602fbfa029ed52c92f13ce3c9f417d303006381"},
|
{file = "onnxruntime-1.17.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83c35809cda898c5a11911c69ceac8a2ac3925911854c526f73bad884582f911"},
|
||||||
{file = "onnxruntime-1.17.1-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dae9071e3facdf2920769dceee03b71c684b6439021defa45b830d05e148924"},
|
{file = "onnxruntime-1.17.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fa464aa4d81df818375239e481887b656e261377d5b6b9a4692466f5f3261edc"},
|
||||||
{file = "onnxruntime-1.17.1-cp38-cp38-win32.whl", hash = "sha256:835d38fa1064841679433b1aa8138b5e1218ddf0cfa7a3ae0d056d8fd9cec713"},
|
{file = "onnxruntime-1.17.0-cp38-cp38-win32.whl", hash = "sha256:b7b337cd0586f7836601623cbd30a443df9528ef23965860d11c753ceeb009f2"},
|
||||||
{file = "onnxruntime-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:96621e0c555c2453bf607606d08af3f70fbf6f315230c28ddea91754e17ad4e6"},
|
{file = "onnxruntime-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:fbb9faaf51d01aa2c147ef52524d9326744c852116d8005b9041809a71838878"},
|
||||||
{file = "onnxruntime-1.17.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:7a9539935fb2d78ebf2cf2693cad02d9930b0fb23cdd5cf37a7df813e977674d"},
|
{file = "onnxruntime-1.17.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:5a06ab84eaa350bf64b1d747b33ccf10da64221ed1f38f7287f15eccbec81603"},
|
||||||
{file = "onnxruntime-1.17.1-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45c6a384e9d9a29c78afff62032a46a993c477b280247a7e335df09372aedbe9"},
|
{file = "onnxruntime-1.17.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d3d11db2c8242766212a68d0b139745157da7ce53bd96ba349a5c65e5a02357"},
|
||||||
{file = "onnxruntime-1.17.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e19f966450f16863a1d6182a685ca33ae04d7772a76132303852d05b95411ea"},
|
{file = "onnxruntime-1.17.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5632077c3ab8b0cd4f74b0af9c4e924be012b1a7bcd7daa845763c6c6bf14b7d"},
|
||||||
{file = "onnxruntime-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e2ae712d64a42aac29ed7a40a426cb1e624a08cfe9273dcfe681614aa65b07dc"},
|
{file = "onnxruntime-1.17.0-cp39-cp39-win32.whl", hash = "sha256:61a12732cba869b3ad2d4e29ab6cb62c7a96f61b8c213f7fcb961ba412b70b37"},
|
||||||
{file = "onnxruntime-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:f7e9f7fb049825cdddf4a923cfc7c649d84d63c0134315f8e0aa9e0c3004672c"},
|
{file = "onnxruntime-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:461fa0fc7d9c392c352b6cccdedf44d818430f3d6eacd924bb804fdea2dcfd02"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2032,21 +2030,21 @@ sympy = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onnxruntime-gpu"
|
name = "onnxruntime-gpu"
|
||||||
version = "1.17.1"
|
version = "1.17.0"
|
||||||
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "onnxruntime_gpu-1.17.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e34ecb2b527ee1265135ae74cd99ea198ff344b8221929a920596a1e461e2bbb"},
|
{file = "onnxruntime_gpu-1.17.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1f2a4e0468ac0bd8246996c3d5dbba92cbbaca874bcd7f9cee4e99ce6eb27f5b"},
|
||||||
{file = "onnxruntime_gpu-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:37786c0f225be90da0a66ca413fe125a925a0900263301cc4dbcad4ff0404673"},
|
{file = "onnxruntime_gpu-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:0721b7930d7abed3730b2335e639e60d94ec411bb4d35a0347cc9c8b52c34540"},
|
||||||
{file = "onnxruntime_gpu-1.17.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3bde190a683ec84ecf61bd390f3c275d388efe72404633df374c52c557ce6d4d"},
|
{file = "onnxruntime_gpu-1.17.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:be0314afe399943904de7c1ca797cbcc63e6fad60eb85d3df6422f81dd94e79e"},
|
||||||
{file = "onnxruntime_gpu-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:5206c84caa770efcc2ca819f71ec007a244ed748ca04e7ff76b86df1a096d2c8"},
|
{file = "onnxruntime_gpu-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:52125c24b21406d1431e43de1c98cea29c21e0cceba80db530b7e4c9216d86ea"},
|
||||||
{file = "onnxruntime_gpu-1.17.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0396ec73de565a64509d96dff154f531f8da8023c191f771ceba47a3f4efc266"},
|
{file = "onnxruntime_gpu-1.17.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:bb802d8033885c412269f8bc8877d8779b0dc874df6fb9df8b796cba7276ad66"},
|
||||||
{file = "onnxruntime_gpu-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:8531d4a833c8e978c5ff1de7b3bcc4126bbe58ea71fae54ddce58fe8777cb136"},
|
{file = "onnxruntime_gpu-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:8c43533e3e5335eaa78059fb86b849a4faded513a00c1feaaa205ca5af51c40f"},
|
||||||
{file = "onnxruntime_gpu-1.17.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7b831f9eafd626f3d44955420a4b1b84f9ffcb987712a0ab6a37d1ee9f2f7a45"},
|
{file = "onnxruntime_gpu-1.17.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:1d461455bba160836d6c11c648c8fd4e4500d5c17096a13e6c2c9d22a4abd436"},
|
||||||
{file = "onnxruntime_gpu-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:a389334d3797519d4b12077db32b8764f1ce54374d0f89235edc04efe8bc192c"},
|
{file = "onnxruntime_gpu-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4398f2175a92f4b35d95279a6294a89c462f24de058a2736ee1d498bab5a16"},
|
||||||
{file = "onnxruntime_gpu-1.17.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:27aeaa36385e459b3867577ed7f68c1756de79aa68f57141d4ae2a31c84f6a33"},
|
{file = "onnxruntime_gpu-1.17.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1d0e3805cd1c024aba7f4ae576fd08545fc27530a2aaad2b3c8ac0ee889fbd05"},
|
||||||
{file = "onnxruntime_gpu-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b46094ea348aff6c6494402ac4260e2d2aba0522ae13e1ae29d98a29384ed70"},
|
{file = "onnxruntime_gpu-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc1da5b93363ee600b5b220b04eeec51ad2c2b3e96f0b7615b16b8a173c88001"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2057,11 +2055,6 @@ packaging = "*"
|
|||||||
protobuf = "*"
|
protobuf = "*"
|
||||||
sympy = "*"
|
sympy = "*"
|
||||||
|
|
||||||
[package.source]
|
|
||||||
type = "legacy"
|
|
||||||
url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple"
|
|
||||||
reference = "cuda12"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onnxruntime-openvino"
|
name = "onnxruntime-openvino"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
@@ -2108,61 +2101,61 @@ numpy = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orjson"
|
name = "orjson"
|
||||||
version = "3.9.15"
|
version = "3.9.14"
|
||||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "orjson-3.9.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d61f7ce4727a9fa7680cd6f3986b0e2c732639f46a5e0156e550e35258aa313a"},
|
{file = "orjson-3.9.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:793f6c9448ab6eb7d4974b4dde3f230345c08ca6c7995330fbceeb43a5c8aa5e"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4feeb41882e8aa17634b589533baafdceb387e01e117b1ec65534ec724023d04"},
|
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bc7928d161840096adc956703494b5c0193ede887346f028216cac0af87500"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbbeb3c9b2edb5fd044b2a070f127a0ac456ffd079cb82746fc84af01ef021a4"},
|
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58b36f54da759602d8e2f7dad958752d453dfe2c7122767bc7f765e17dc59959"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b66bcc5670e8a6b78f0313bcb74774c8291f6f8aeef10fe70e910b8040f3ab75"},
|
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abcda41ecdc950399c05eff761c3de91485d9a70d8227cb599ad3a66afe93bcc"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2973474811db7b35c30248d1129c64fd2bdf40d57d84beed2a9a379a6f57d0ab"},
|
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df76ecd17b1b3627bddfd689faaf206380a1a38cc9f6c4075bd884eaedcf46c2"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe41b6f72f52d3da4db524c8653e46243c8c92df826ab5ffaece2dba9cccd58"},
|
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d450a8e0656efb5d0fcb062157b918ab02dcca73278975b4ee9ea49e2fcf5bd5"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4228aace81781cc9d05a3ec3a6d2673a1ad0d8725b4e915f1089803e9efd2b99"},
|
{file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:95c03137b0cf66517c8baa65770507a756d3a89489d8ecf864ea92348e1beabe"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f7b65bfaf69493c73423ce9db66cfe9138b2f9ef62897486417a8fcb0a92bfe"},
|
{file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20837e10835c98973673406d6798e10f821e7744520633811a5a3d809762d8cc"},
|
||||||
{file = "orjson-3.9.15-cp310-none-win32.whl", hash = "sha256:2d99e3c4c13a7b0fb3792cc04c2829c9db07838fb6973e578b85c1745e7d0ce7"},
|
{file = "orjson-3.9.14-cp310-none-win32.whl", hash = "sha256:1f7b6f3ef10ae8e3558abb729873d033dbb5843507c66b1c0767e32502ba96bb"},
|
||||||
{file = "orjson-3.9.15-cp310-none-win_amd64.whl", hash = "sha256:b725da33e6e58e4a5d27958568484aa766e825e93aa20c26c91168be58e08cbb"},
|
{file = "orjson-3.9.14-cp310-none-win_amd64.whl", hash = "sha256:ea890e6dc1711aeec0a33b8520e395c2f3d59ead5b4351a788e06bf95fc7ba81"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c8e8fe01e435005d4421f183038fc70ca85d2c1e490f51fb972db92af6e047c2"},
|
{file = "orjson-3.9.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c19009ff37f033c70acd04b636380379499dac2cba27ae7dfc24f304deabbc81"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87f1097acb569dde17f246faa268759a71a2cb8c96dd392cd25c668b104cad2f"},
|
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19cdea0664aec0b7f385be84986d4defd3334e9c3c799407686ee1c26f7b8251"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff0f9913d82e1d1fadbd976424c316fbc4d9c525c81d047bbdd16bd27dd98cfc"},
|
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:135d518f73787ce323b1a5e21fb854fe22258d7a8ae562b81a49d6c7f826f2a3"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8055ec598605b0077e29652ccfe9372247474375e0e3f5775c91d9434e12d6b1"},
|
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2cf1d0557c61c75e18cf7d69fb689b77896e95553e212c0cc64cf2087944b84"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6768a327ea1ba44c9114dba5fdda4a214bdb70129065cd0807eb5f010bfcbb5"},
|
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7c11667421df2d8b18b021223505dcc3ee51be518d54e4dc49161ac88ac2b87"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12365576039b1a5a47df01aadb353b68223da413e2e7f98c02403061aad34bde"},
|
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eefc41ba42e75ed88bc396d8fe997beb20477f3e7efa000cd7a47eda452fbb2"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71c6b009d431b3839d7c14c3af86788b3cfac41e969e3e1c22f8a6ea13139404"},
|
{file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:917311d6a64d1c327c0dfda1e41f3966a7fb72b11ca7aa2e7a68fcccc7db35d9"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e18668f1bd39e69b7fed19fa7cd1cd110a121ec25439328b5c89934e6d30d357"},
|
{file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4dc1c132259b38d12c6587d190cd09cd76e3b5273ce71fe1372437b4cbc65f6f"},
|
||||||
{file = "orjson-3.9.15-cp311-none-win32.whl", hash = "sha256:62482873e0289cf7313461009bf62ac8b2e54bc6f00c6fabcde785709231a5d7"},
|
{file = "orjson-3.9.14-cp311-none-win32.whl", hash = "sha256:6f39a10408478f4c05736a74da63727a1ae0e83e3533d07b19443400fe8591ca"},
|
||||||
{file = "orjson-3.9.15-cp311-none-win_amd64.whl", hash = "sha256:b3d336ed75d17c7b1af233a6561cf421dee41d9204aa3cfcc6c9c65cd5bb69a8"},
|
{file = "orjson-3.9.14-cp311-none-win_amd64.whl", hash = "sha256:26280a7fcb62d8257f634c16acebc3bec626454f9ab13558bbf7883b9140760e"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:82425dd5c7bd3adfe4e94c78e27e2fa02971750c2b7ffba648b0f5d5cc016a73"},
|
{file = "orjson-3.9.14-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:08e722a8d06b13b67a51f247a24938d1a94b4b3862e40e0eef3b2e98c99cd04c"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c51378d4a8255b2e7c1e5cc430644f0939539deddfa77f6fac7b56a9784160a"},
|
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2591faa0c031cf3f57e5bce1461cfbd6160f3f66b5a72609a130924917cb07d"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae4e06be04dc00618247c4ae3f7c3e561d5bc19ab6941427f6d3722a0875ef7"},
|
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2450d87dd7b4f277f4c5598faa8b49a0c197b91186c47a2c0b88e15531e4e3e"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcef128f970bb63ecf9a65f7beafd9b55e3aaf0efc271a4154050fc15cdb386e"},
|
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90903d2908158a2c9077a06f11e27545de610af690fb178fd3ba6b32492d4d1c"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b72758f3ffc36ca566ba98a8e7f4f373b6c17c646ff8ad9b21ad10c29186f00d"},
|
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce6f095eef0026eae76fc212f20f786011ecf482fc7df2f4c272a8ae6dd7b1ef"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c57bc7b946cf2efa67ac55766e41764b66d40cbd9489041e637c1304400494"},
|
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:751250a31fef2bac05a2da2449aae7142075ea26139271f169af60456d8ad27a"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:946c3a1ef25338e78107fba746f299f926db408d34553b4754e90a7de1d44068"},
|
{file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a1af21160a38ee8be3f4fcf24ee4b99e6184cadc7f915d599f073f478a94d2c"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f256d03957075fcb5923410058982aea85455d035607486ccb847f095442bda"},
|
{file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:449bf090b2aa4e019371d7511a6ea8a5a248139205c27d1834bb4b1e3c44d936"},
|
||||||
{file = "orjson-3.9.15-cp312-none-win_amd64.whl", hash = "sha256:5bb399e1b49db120653a31463b4a7b27cf2fbfe60469546baf681d1b39f4edf2"},
|
{file = "orjson-3.9.14-cp312-none-win_amd64.whl", hash = "sha256:a603161318ff699784943e71f53899983b7dee571b4dd07c336437c9c5a272b0"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b17f0f14a9c0ba55ff6279a922d1932e24b13fc218a3e968ecdbf791b3682b25"},
|
{file = "orjson-3.9.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:814f288c011efdf8f115c5ebcc1ab94b11da64b207722917e0ceb42f52ef30a3"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f6cbd8e6e446fb7e4ed5bac4661a29e43f38aeecbf60c4b900b825a353276a1"},
|
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88cafb100af68af3b9b29b5ccd09fdf7a48c63327916c8c923a94c336d38dd3"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76bc6356d07c1d9f4b782813094d0caf1703b729d876ab6a676f3aaa9a47e37c"},
|
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba3518b999f88882ade6686f1b71e207b52e23546e180499be5bbb63a2f9c6e6"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdfa97090e2d6f73dced247a2f2d8004ac6449df6568f30e7fa1a045767c69a6"},
|
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978f416bbff9da8d2091e3cf011c92da68b13f2c453dcc2e8109099b2a19d234"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7413070a3e927e4207d00bd65f42d1b780fb0d32d7b1d951f6dc6ade318e1b5a"},
|
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75fc593cf836f631153d0e21beaeb8d26e144445c73645889335c2247fcd71a0"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cf1596680ac1f01839dba32d496136bdd5d8ffb858c280fa82bbfeb173bdd40"},
|
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d1528db3c7554f9d6eeb09df23cb80dd5177ec56eeb55cc5318826928de506"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:809d653c155e2cc4fd39ad69c08fdff7f4016c355ae4b88905219d3579e31eb7"},
|
{file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7183cc68ee2113b19b0b8714221e5e3b07b3ba10ca2bb108d78fd49cefaae101"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:920fa5a0c5175ab14b9c78f6f820b75804fb4984423ee4c4f1e6d748f8b22bc1"},
|
{file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df3266d54246cb56b8bb17fa908660d2a0f2e3f63fbc32451ffc1b1505051d07"},
|
||||||
{file = "orjson-3.9.15-cp38-none-win32.whl", hash = "sha256:2b5c0f532905e60cf22a511120e3719b85d9c25d0e1c2a8abb20c4dede3b05a5"},
|
{file = "orjson-3.9.14-cp38-none-win32.whl", hash = "sha256:7913079b029e1b3501854c9a78ad938ed40d61fe09bebab3c93e60ff1301b189"},
|
||||||
{file = "orjson-3.9.15-cp38-none-win_amd64.whl", hash = "sha256:67384f588f7f8daf040114337d34a5188346e3fae6c38b6a19a2fe8c663a2f9b"},
|
{file = "orjson-3.9.14-cp38-none-win_amd64.whl", hash = "sha256:29512eb925b620e5da2fd7585814485c67cc6ba4fe739a0a700c50467a8a8065"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6fc2fe4647927070df3d93f561d7e588a38865ea0040027662e3e541d592811e"},
|
{file = "orjson-3.9.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5bf597530544db27a8d76aced49cfc817ee9503e0a4ebf0109cd70331e7bbe0c"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34cbcd216e7af5270f2ffa63a963346845eb71e174ea530867b7443892d77180"},
|
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac650d49366fa41fe702e054cb560171a8634e2865537e91f09a8d05ea5b1d37"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f541587f5c558abd93cb0de491ce99a9ef8d1ae29dd6ab4dbb5a13281ae04cbd"},
|
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:236230433a9a4968ab895140514c308fdf9f607cb8bee178a04372b771123860"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92255879280ef9c3c0bcb327c5a1b8ed694c290d61a6a532458264f887f052cb"},
|
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3014ccbda9be0b1b5f8ea895121df7e6524496b3908f4397ff02e923bcd8f6dd"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a1f57fb601c426635fcae9ddbe90dfc1ed42245eb4c75e4960440cac667262"},
|
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac0c7eae7ad3a223bde690565442f8a3d620056bd01196f191af8be58a5248e1"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede0bde16cc6e9b96633df1631fbcd66491d1063667f260a4f2386a098393790"},
|
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca33fdd0b38839b01912c57546d4f412ba7bfa0faf9bf7453432219aec2df07"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e88b97ef13910e5f87bcbc4dd7979a7de9ba8702b54d3204ac587e83639c0c2b"},
|
{file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f75823cc1674a840a151e999a7dfa0d86c911150dd6f951d0736ee9d383bf415"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57d5d8cf9c27f7ef6bc56a5925c7fbc76b61288ab674eb352c26ac780caa5b10"},
|
{file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f52ac2eb49e99e7373f62e2a68428c6946cda52ce89aa8fe9f890c7278e2d3a"},
|
||||||
{file = "orjson-3.9.15-cp39-none-win32.whl", hash = "sha256:001f4eb0ecd8e9ebd295722d0cbedf0748680fb9998d3993abaed2f40587257a"},
|
{file = "orjson-3.9.14-cp39-none-win32.whl", hash = "sha256:0572f174f50b673b7df78680fb52cd0087a8585a6d06d295a5f790568e1064c6"},
|
||||||
{file = "orjson-3.9.15-cp39-none-win_amd64.whl", hash = "sha256:ea0b183a5fe6b2b45f3b854b0d19c4e932d6f5934ae1f723b07cf9560edd4ec7"},
|
{file = "orjson-3.9.14-cp39-none-win_amd64.whl", hash = "sha256:ab90c02cb264250b8a58cedcc72ed78a4a257d956c8d3c8bebe9751b818dfad8"},
|
||||||
{file = "orjson-3.9.15.tar.gz", hash = "sha256:95cae920959d772f30ab36d3b25f83bb0f3be671e986c72ce22f8fa700dae061"},
|
{file = "orjson-3.9.14.tar.gz", hash = "sha256:06fb40f8e49088ecaa02f1162581d39e2cf3fd9dbbfe411eb2284147c99bad79"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2472,13 +2465,13 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "8.0.2"
|
version = "8.0.0"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"},
|
{file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"},
|
||||||
{file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"},
|
{file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2815,13 +2808,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "13.7.1"
|
version = "13.7.0"
|
||||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7.0"
|
python-versions = ">=3.7.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
|
{file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"},
|
||||||
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
|
{file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2843,28 +2836,28 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.3.0"
|
version = "0.2.1"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944"},
|
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"},
|
||||||
{file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19"},
|
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"},
|
||||||
{file = "ruff-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"},
|
||||||
{file = "ruff-0.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"},
|
||||||
{file = "ruff-0.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"},
|
||||||
{file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"},
|
||||||
{file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"},
|
||||||
{file = "ruff-0.3.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"},
|
||||||
{file = "ruff-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"},
|
||||||
{file = "ruff-0.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e"},
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"},
|
||||||
{file = "ruff-0.3.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933"},
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"},
|
||||||
{file = "ruff-0.3.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2"},
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"},
|
||||||
{file = "ruff-0.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f"},
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"},
|
||||||
{file = "ruff-0.3.0-py3-none-win32.whl", hash = "sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b"},
|
{file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"},
|
||||||
{file = "ruff-0.3.0-py3-none-win_amd64.whl", hash = "sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f"},
|
{file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"},
|
||||||
{file = "ruff-0.3.0-py3-none-win_arm64.whl", hash = "sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"},
|
{file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"},
|
||||||
{file = "ruff-0.3.0.tar.gz", hash = "sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a"},
|
{file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3626,4 +3619,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.10,<3.12"
|
python-versions = ">=3.10,<3.12"
|
||||||
content-hash = "c947090d326e81179054b7ce4dded311df8b7ca5a56680d5e9459cf8ca18df1a"
|
content-hash = "c982d5c5fee76ca102d823010a538f287ac98583f330ebee3c0775c5f42f117d"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.98.2"
|
version = "1.95.1"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -45,7 +45,7 @@ onnxruntime = "^1.15.0"
|
|||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[tool.poetry.group.cuda.dependencies]
|
[tool.poetry.group.cuda.dependencies]
|
||||||
onnxruntime-gpu = {version = "^1.17.0", source = "cuda12"}
|
onnxruntime-gpu = "^1.15.0"
|
||||||
|
|
||||||
[tool.poetry.group.openvino]
|
[tool.poetry.group.openvino]
|
||||||
optional = true
|
optional = true
|
||||||
@@ -59,11 +59,6 @@ optional = true
|
|||||||
[tool.poetry.group.armnn.dependencies]
|
[tool.poetry.group.armnn.dependencies]
|
||||||
onnxruntime = "^1.15.0"
|
onnxruntime = "^1.15.0"
|
||||||
|
|
||||||
[[tool.poetry.source]]
|
|
||||||
name = "cuda12"
|
|
||||||
url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/"
|
|
||||||
priority = "explicit"
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|||||||
@@ -62,13 +62,8 @@ fi
|
|||||||
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
|
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
|
||||||
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
|
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
|
||||||
npm --prefix server version "$SERVER_PUMP"
|
npm --prefix server version "$SERVER_PUMP"
|
||||||
make open-api
|
|
||||||
npm --prefix open-api/typescript-sdk version "$SERVER_PUMP"
|
|
||||||
npm --prefix web version "$SERVER_PUMP"
|
npm --prefix web version "$SERVER_PUMP"
|
||||||
npm --prefix e2e version "$SERVER_PUMP"
|
make open-api
|
||||||
npm --prefix web i --package-lock-only
|
|
||||||
npm --prefix cli i --package-lock-only
|
|
||||||
npm --prefix e2e i --package-lock-only
|
|
||||||
poetry --directory machine-learning version "$SERVER_PUMP"
|
poetry --directory machine-learning version "$SERVER_PUMP"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 128,
|
"android.injected.version.code" => 123,
|
||||||
"android.injected.version.name" => "1.98.2",
|
"android.injected.version.name" => "1.95.1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -5,17 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000215">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000232">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="77.732834">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="78.881681">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="37.34283">
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="32.080999">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "DETALLS",
|
"exif_bottom_sheet_details": "DETALLS",
|
||||||
"exif_bottom_sheet_location": "UBICACIÓ",
|
"exif_bottom_sheet_location": "UBICACIÓ",
|
||||||
"exif_bottom_sheet_location_add": "Add a location",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||||
"experimental_settings_subtitle": "Use at your own risk!",
|
"experimental_settings_subtitle": "Use at your own risk!",
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "PODROBNOSTI",
|
"exif_bottom_sheet_details": "PODROBNOSTI",
|
||||||
"exif_bottom_sheet_location": "LOKALITA",
|
"exif_bottom_sheet_location": "LOKALITA",
|
||||||
"exif_bottom_sheet_location_add": "Přidat polohu",
|
"exif_bottom_sheet_location_add": "Přidat polohu",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Zpracovávám",
|
"experimental_settings_new_asset_list_subtitle": "Zpracovávám",
|
||||||
"experimental_settings_new_asset_list_title": "Povolení experimentální mřížky fotografií",
|
"experimental_settings_new_asset_list_title": "Povolení experimentální mřížky fotografií",
|
||||||
"experimental_settings_subtitle": "Používejte na vlastní riziko!",
|
"experimental_settings_subtitle": "Používejte na vlastní riziko!",
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
"app_bar_signout_dialog_title": "Log ud",
|
"app_bar_signout_dialog_title": "Log ud",
|
||||||
"archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet",
|
"archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet",
|
||||||
"archive_page_title": "Arkivér ({})",
|
"archive_page_title": "Arkivér ({})",
|
||||||
"asset_action_delete_err_read_only": "Kan ikke slette kun læselige elementer. Springer over",
|
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
|
||||||
"asset_action_share_err_offline": "Kan ikke hente offline element(er). Springer over",
|
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout",
|
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout",
|
||||||
"asset_list_layout_settings_group_automatically": "Automatisk",
|
"asset_list_layout_settings_group_automatically": "Automatisk",
|
||||||
"asset_list_layout_settings_group_by": "Gruppér elementer pr. ",
|
"asset_list_layout_settings_group_by": "Gruppér elementer pr. ",
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
"control_bottom_app_bar_share": "Del",
|
"control_bottom_app_bar_share": "Del",
|
||||||
"control_bottom_app_bar_share_to": "Del til",
|
"control_bottom_app_bar_share_to": "Del til",
|
||||||
"control_bottom_app_bar_stack": "Stak",
|
"control_bottom_app_bar_stack": "Stak",
|
||||||
"control_bottom_app_bar_trash_from_immich": "Flyt til papirkurv",
|
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
|
||||||
"control_bottom_app_bar_unarchive": "Afakivér",
|
"control_bottom_app_bar_unarchive": "Afakivér",
|
||||||
"control_bottom_app_bar_unfavorite": "Fjern favorit",
|
"control_bottom_app_bar_unfavorite": "Fjern favorit",
|
||||||
"control_bottom_app_bar_upload": "Upload",
|
"control_bottom_app_bar_upload": "Upload",
|
||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "DETALJER",
|
"exif_bottom_sheet_details": "DETALJER",
|
||||||
"exif_bottom_sheet_location": "LOKATION",
|
"exif_bottom_sheet_location": "LOKATION",
|
||||||
"exif_bottom_sheet_location_add": "Tilføj en placering",
|
"exif_bottom_sheet_location_add": "Tilføj en placering",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Under udarbejdelse",
|
"experimental_settings_new_asset_list_subtitle": "Under udarbejdelse",
|
||||||
"experimental_settings_new_asset_list_title": "Aktiver eksperimentelt fotogitter",
|
"experimental_settings_new_asset_list_title": "Aktiver eksperimentelt fotogitter",
|
||||||
"experimental_settings_subtitle": "Brug på eget ansvar!",
|
"experimental_settings_subtitle": "Brug på eget ansvar!",
|
||||||
@@ -200,7 +199,7 @@
|
|||||||
"home_page_archive_err_partner": "Kan endnu ikke arkivere partners elementer. Springer over",
|
"home_page_archive_err_partner": "Kan endnu ikke arkivere partners elementer. Springer over",
|
||||||
"home_page_building_timeline": "Bygger tidslinjen",
|
"home_page_building_timeline": "Bygger tidslinjen",
|
||||||
"home_page_delete_err_partner": "Kan endnu ikke slette partners elementer. Springer over",
|
"home_page_delete_err_partner": "Kan endnu ikke slette partners elementer. Springer over",
|
||||||
"home_page_delete_remote_err_local": "Lokale elementer i fjernsletningssektion. Springer over",
|
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
|
||||||
"home_page_favorite_err_local": "Kan endnu ikke gøre lokale elementer til favoritter. Springer over..",
|
"home_page_favorite_err_local": "Kan endnu ikke gøre lokale elementer til favoritter. Springer over..",
|
||||||
"home_page_favorite_err_partner": "Kan endnu ikke tilføje partners elementer som favoritter. Springer over",
|
"home_page_favorite_err_partner": "Kan endnu ikke tilføje partners elementer som favoritter. Springer over",
|
||||||
"home_page_first_time_notice": "Hvis det er din første gang i appen, bedes du vælge en sikkerhedskopi af albummer så tidlinjen kan blive fyldt med billeder og videoer fra albummerne.",
|
"home_page_first_time_notice": "Hvis det er din første gang i appen, bedes du vælge en sikkerhedskopi af albummer så tidlinjen kan blive fyldt med billeder og videoer fra albummerne.",
|
||||||
@@ -280,8 +279,8 @@
|
|||||||
"map_zoom_to_see_photos": "Zoom ud for at vise billeder",
|
"map_zoom_to_see_photos": "Zoom ud for at vise billeder",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"motion_photos_page_title": "Bevægelsesbilleder",
|
"motion_photos_page_title": "Bevægelsesbilleder",
|
||||||
"multiselect_grid_edit_date_time_err_read_only": "Kan ikke redigere datoen på kun læselige elementer. Springer over",
|
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||||
"multiselect_grid_edit_gps_err_read_only": "Kan ikke redigere lokation af kun læselige elementer. Springer over",
|
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
||||||
"notification_permission_dialog_cancel": "Annuller",
|
"notification_permission_dialog_cancel": "Annuller",
|
||||||
"notification_permission_dialog_content": "Gå til indstillinger for at slå notifikationer til.",
|
"notification_permission_dialog_content": "Gå til indstillinger for at slå notifikationer til.",
|
||||||
"notification_permission_dialog_settings": "Indstillinger",
|
"notification_permission_dialog_settings": "Indstillinger",
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "DETAILS",
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
"exif_bottom_sheet_location": "STANDORT",
|
"exif_bottom_sheet_location": "STANDORT",
|
||||||
"exif_bottom_sheet_location_add": "Aufnahmeort hinzufügen",
|
"exif_bottom_sheet_location_add": "Aufnahmeort hinzufügen",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "In Arbeit",
|
"experimental_settings_new_asset_list_subtitle": "In Arbeit",
|
||||||
"experimental_settings_new_asset_list_title": "Experimentelles Fotogitter aktivieren",
|
"experimental_settings_new_asset_list_title": "Experimentelles Fotogitter aktivieren",
|
||||||
"experimental_settings_subtitle": "Benutzung auf eigene Gefahr!",
|
"experimental_settings_subtitle": "Benutzung auf eigene Gefahr!",
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "DETAILS",
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
"exif_bottom_sheet_location": "LOCATION",
|
"exif_bottom_sheet_location": "LOCATION",
|
||||||
"exif_bottom_sheet_location_add": "Add a location",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||||
"experimental_settings_subtitle": "Use at your own risk!",
|
"experimental_settings_subtitle": "Use at your own risk!",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"action_common_cancel": "Cancelar",
|
"action_common_cancel": "Cancel",
|
||||||
"action_common_update": "Actualizar",
|
"action_common_update": "Update",
|
||||||
"add_to_album_bottom_sheet_added": "Agregado a {album}",
|
"add_to_album_bottom_sheet_added": "Agregado a {album}",
|
||||||
"add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}",
|
"add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}",
|
||||||
"advanced_settings_log_level_title": "Nivel de log: {}",
|
"advanced_settings_log_level_title": "Nivel de log: {}",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"app_bar_signout_dialog_title": "Cerrar sesión",
|
"app_bar_signout_dialog_title": "Cerrar sesión",
|
||||||
"archive_page_no_archived_assets": "No se encontraron recursos archivados",
|
"archive_page_no_archived_assets": "No se encontraron recursos archivados",
|
||||||
"archive_page_title": "Archivo ({})",
|
"archive_page_title": "Archivo ({})",
|
||||||
"asset_action_delete_err_read_only": "No se pueden borrar los archivos de solo lectura. Saltando.",
|
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
|
||||||
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||||
"asset_list_layout_settings_dynamic_layout_title": "Diseño dinámico",
|
"asset_list_layout_settings_dynamic_layout_title": "Diseño dinámico",
|
||||||
"asset_list_layout_settings_group_automatically": "Automatico",
|
"asset_list_layout_settings_group_automatically": "Automatico",
|
||||||
@@ -142,15 +142,15 @@
|
|||||||
"control_bottom_app_bar_archive": "Archivar",
|
"control_bottom_app_bar_archive": "Archivar",
|
||||||
"control_bottom_app_bar_create_new_album": "Crear nuevo álbum",
|
"control_bottom_app_bar_create_new_album": "Crear nuevo álbum",
|
||||||
"control_bottom_app_bar_delete": "Eliminar",
|
"control_bottom_app_bar_delete": "Eliminar",
|
||||||
"control_bottom_app_bar_delete_from_immich": "Borrar de Immich",
|
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||||
"control_bottom_app_bar_delete_from_local": "Borrar del dispositivo",
|
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||||
"control_bottom_app_bar_edit_location": "Editar ubicación",
|
"control_bottom_app_bar_edit_location": "Editar ubicación",
|
||||||
"control_bottom_app_bar_edit_time": "Editar fecha y hora",
|
"control_bottom_app_bar_edit_time": "Editar fecha y hora",
|
||||||
"control_bottom_app_bar_favorite": "Favorito",
|
"control_bottom_app_bar_favorite": "Favorito",
|
||||||
"control_bottom_app_bar_share": "Compartir",
|
"control_bottom_app_bar_share": "Compartir",
|
||||||
"control_bottom_app_bar_share_to": "Enviar",
|
"control_bottom_app_bar_share_to": "Enviar",
|
||||||
"control_bottom_app_bar_stack": "Apilar",
|
"control_bottom_app_bar_stack": "Apilar",
|
||||||
"control_bottom_app_bar_trash_from_immich": "Mover a la papelera",
|
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
|
||||||
"control_bottom_app_bar_unarchive": "Desarchivar",
|
"control_bottom_app_bar_unarchive": "Desarchivar",
|
||||||
"control_bottom_app_bar_unfavorite": "Unfavorite",
|
"control_bottom_app_bar_unfavorite": "Unfavorite",
|
||||||
"control_bottom_app_bar_upload": "Subir",
|
"control_bottom_app_bar_upload": "Subir",
|
||||||
@@ -165,9 +165,9 @@
|
|||||||
"daily_title_text_date_year": "E dd de MMM, yyyy",
|
"daily_title_text_date_year": "E dd de MMM, yyyy",
|
||||||
"date_format": "E d, LLL y • h:mm a",
|
"date_format": "E d, LLL y • h:mm a",
|
||||||
"delete_dialog_alert": "Estos elementos serán eliminados permanentemente de Immich y de tu dispositivo",
|
"delete_dialog_alert": "Estos elementos serán eliminados permanentemente de Immich y de tu dispositivo",
|
||||||
"delete_dialog_alert_local": "Estas imágenes van a ser borradas de tu dispositivo, pero seguirán disponibles en el servidor Immich",
|
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
|
||||||
"delete_dialog_alert_local_non_backed_up": "Algunas de las imágenes no tienen copia de seguridad y serán borradas de forma permanente de tu dispositivo",
|
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
|
||||||
"delete_dialog_alert_remote": "Estas imágenes van a ser borradas de forma permanente del servidor Immich",
|
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
|
||||||
"delete_dialog_cancel": "Cancelar",
|
"delete_dialog_cancel": "Cancelar",
|
||||||
"delete_dialog_ok": "Eliminar",
|
"delete_dialog_ok": "Eliminar",
|
||||||
"delete_dialog_ok_force": "Delete Anyway",
|
"delete_dialog_ok_force": "Delete Anyway",
|
||||||
@@ -178,14 +178,13 @@
|
|||||||
"delete_shared_link_dialog_title": "Eliminar enlace compartido",
|
"delete_shared_link_dialog_title": "Eliminar enlace compartido",
|
||||||
"description_input_hint_text": "Agregar descripción...",
|
"description_input_hint_text": "Agregar descripción...",
|
||||||
"description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
|
"description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
|
||||||
"edit_date_time_dialog_date_time": "Fecha y Hora",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Zona horaria",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_location_dialog_title": "Ubicación",
|
"edit_location_dialog_title": "Location",
|
||||||
"exif_bottom_sheet_description": "Agregar Descripción...",
|
"exif_bottom_sheet_description": "Agregar Descripción...",
|
||||||
"exif_bottom_sheet_details": "DETALLES",
|
"exif_bottom_sheet_details": "DETALLES",
|
||||||
"exif_bottom_sheet_location": "UBICACIÓN",
|
"exif_bottom_sheet_location": "UBICACIÓN",
|
||||||
"exif_bottom_sheet_location_add": "Añadir ubicación",
|
"exif_bottom_sheet_location_add": "Añadir ubicación",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
|
"experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
|
||||||
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
|
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
|
||||||
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
|
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
|
||||||
@@ -221,13 +220,13 @@
|
|||||||
"library_page_sort_most_oldest_photo": "Foto más antigua",
|
"library_page_sort_most_oldest_photo": "Foto más antigua",
|
||||||
"library_page_sort_most_recent_photo": "Foto más reciente",
|
"library_page_sort_most_recent_photo": "Foto más reciente",
|
||||||
"library_page_sort_title": "Título del álbum",
|
"library_page_sort_title": "Título del álbum",
|
||||||
"location_picker_choose_on_map": "Elegir en el mapa",
|
"location_picker_choose_on_map": "Choose on map",
|
||||||
"location_picker_latitude": "Latitud",
|
"location_picker_latitude": "Latitude",
|
||||||
"location_picker_latitude_error": "Introduce una latitud válida",
|
"location_picker_latitude_error": "Enter a valid latitude",
|
||||||
"location_picker_latitude_hint": "Introduce tu latitud aquí",
|
"location_picker_latitude_hint": "Enter your latitude here",
|
||||||
"location_picker_longitude": "Longitud",
|
"location_picker_longitude": "Longitude",
|
||||||
"location_picker_longitude_error": "Introduce una longitud válida",
|
"location_picker_longitude_error": "Enter a valid longitude",
|
||||||
"location_picker_longitude_hint": "Introduce tu longitud aquí",
|
"location_picker_longitude_hint": "Enter your longitude here",
|
||||||
"login_disabled": "El inicio de sesión ha sido desactivado",
|
"login_disabled": "El inicio de sesión ha sido desactivado",
|
||||||
"login_form_api_exception": "Excepción producida por API. Por favor, verifica el URL del servidor e inténtalo de nuevo.",
|
"login_form_api_exception": "Excepción producida por API. Por favor, verifica el URL del servidor e inténtalo de nuevo.",
|
||||||
"login_form_back_button_text": "Atrás",
|
"login_form_back_button_text": "Atrás",
|
||||||
@@ -253,12 +252,12 @@
|
|||||||
"login_form_server_error": "No se pudo conectar al servidor.",
|
"login_form_server_error": "No se pudo conectar al servidor.",
|
||||||
"login_password_changed_error": "Hubo un error actualizando la contraseña",
|
"login_password_changed_error": "Hubo un error actualizando la contraseña",
|
||||||
"login_password_changed_success": "Contraseña cambiado con éxito",
|
"login_password_changed_success": "Contraseña cambiado con éxito",
|
||||||
"map_assets_in_bound": "{} foto",
|
"map_assets_in_bound": "{} photo",
|
||||||
"map_assets_in_bounds": "{} fotos",
|
"map_assets_in_bounds": "{} photos",
|
||||||
"map_cannot_get_user_location": "No se pudo obtener la posición del usuario",
|
"map_cannot_get_user_location": "No se pudo obtener la posición del usuario",
|
||||||
"map_location_dialog_cancel": "Cancelar",
|
"map_location_dialog_cancel": "Cancelar",
|
||||||
"map_location_dialog_yes": "Sí",
|
"map_location_dialog_yes": "Sí",
|
||||||
"map_location_picker_page_use_location": "Usar esta ubicación",
|
"map_location_picker_page_use_location": "Use this location",
|
||||||
"map_location_service_disabled_content": "Los servicios de ubicación deben estar activados para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?",
|
"map_location_service_disabled_content": "Los servicios de ubicación deben estar activados para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?",
|
||||||
"map_location_service_disabled_title": "Servicios de ubicación desactivados",
|
"map_location_service_disabled_title": "Servicios de ubicación desactivados",
|
||||||
"map_no_assets_in_bounds": "No hay fotos en esta zona",
|
"map_no_assets_in_bounds": "No hay fotos en esta zona",
|
||||||
@@ -266,22 +265,22 @@
|
|||||||
"map_no_location_permission_title": "Permisos de ubicación denegados",
|
"map_no_location_permission_title": "Permisos de ubicación denegados",
|
||||||
"map_settings_dark_mode": "Modo oscuro",
|
"map_settings_dark_mode": "Modo oscuro",
|
||||||
"map_settings_date_range_option_all": "Todo",
|
"map_settings_date_range_option_all": "Todo",
|
||||||
"map_settings_date_range_option_day": "Últimas 24 horas",
|
"map_settings_date_range_option_day": "Past 24 hours",
|
||||||
"map_settings_date_range_option_days": "Últimos {} días",
|
"map_settings_date_range_option_days": "Past {} days",
|
||||||
"map_settings_date_range_option_year": "Último año",
|
"map_settings_date_range_option_year": "Past year",
|
||||||
"map_settings_date_range_option_years": "Últimos {} años",
|
"map_settings_date_range_option_years": "Past {} years",
|
||||||
"map_settings_dialog_cancel": "Cancelar",
|
"map_settings_dialog_cancel": "Cancelar",
|
||||||
"map_settings_dialog_save": "Guardar",
|
"map_settings_dialog_save": "Guardar",
|
||||||
"map_settings_dialog_title": "Ajustes mapa",
|
"map_settings_dialog_title": "Ajustes mapa",
|
||||||
"map_settings_include_show_archived": "Incluir archivados",
|
"map_settings_include_show_archived": "Incluir archivados",
|
||||||
"map_settings_only_relative_range": "Rango de fechas",
|
"map_settings_only_relative_range": "Rango de fechas",
|
||||||
"map_settings_only_show_favorites": "Mostrar solo favoritas",
|
"map_settings_only_show_favorites": "Mostrar solo favoritas",
|
||||||
"map_settings_theme_settings": "Apariencia del Mapa",
|
"map_settings_theme_settings": "Map Theme",
|
||||||
"map_zoom_to_see_photos": "Alejar para ver fotos",
|
"map_zoom_to_see_photos": "Alejar para ver fotos",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"motion_photos_page_title": "Foto en Movimiento",
|
"motion_photos_page_title": "Foto en Movimiento",
|
||||||
"multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha de archivos de solo lectura. Saltando.",
|
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||||
"multiselect_grid_edit_gps_err_read_only": "No se puede cambiar la localización de archivos de solo lectura. Saltando.",
|
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
||||||
"notification_permission_dialog_cancel": "Cancelar",
|
"notification_permission_dialog_cancel": "Cancelar",
|
||||||
"notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.",
|
"notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.",
|
||||||
"notification_permission_dialog_settings": "Ajustes",
|
"notification_permission_dialog_settings": "Ajustes",
|
||||||
@@ -319,7 +318,7 @@
|
|||||||
"profile_drawer_sign_out": "Cerrar Sesión",
|
"profile_drawer_sign_out": "Cerrar Sesión",
|
||||||
"profile_drawer_trash": "Papelera",
|
"profile_drawer_trash": "Papelera",
|
||||||
"recently_added_page_title": "Recién Agregadas",
|
"recently_added_page_title": "Recién Agregadas",
|
||||||
"scaffold_body_error_occurred": "Ha ocurrido un error",
|
"scaffold_body_error_occurred": "Error occurred",
|
||||||
"search_bar_hint": "Busca tus fotos",
|
"search_bar_hint": "Busca tus fotos",
|
||||||
"search_page_categories": "Categorías",
|
"search_page_categories": "Categorías",
|
||||||
"search_page_favorites": "Favoritos",
|
"search_page_favorites": "Favoritos",
|
||||||
@@ -331,9 +330,9 @@
|
|||||||
"search_page_person_add_name_dialog_hint": "Nombre",
|
"search_page_person_add_name_dialog_hint": "Nombre",
|
||||||
"search_page_person_add_name_dialog_save": "Guardar",
|
"search_page_person_add_name_dialog_save": "Guardar",
|
||||||
"search_page_person_add_name_dialog_title": "Añadir nombre",
|
"search_page_person_add_name_dialog_title": "Añadir nombre",
|
||||||
"search_page_person_add_name_subtitle": "Encuéntralos rápido buscando por nombre",
|
"search_page_person_add_name_subtitle": "Find them fast by name with search",
|
||||||
"search_page_person_add_name_title": "Añadir nombre",
|
"search_page_person_add_name_title": "Add a name",
|
||||||
"search_page_person_edit_name": "Cambiar nombre",
|
"search_page_person_edit_name": "Edit name",
|
||||||
"search_page_places": "Lugares",
|
"search_page_places": "Lugares",
|
||||||
"search_page_recently_added": "Recién agregadas",
|
"search_page_recently_added": "Recién agregadas",
|
||||||
"search_page_screenshots": "Capturas de pantalla",
|
"search_page_screenshots": "Capturas de pantalla",
|
||||||
@@ -342,7 +341,7 @@
|
|||||||
"search_page_videos": "Videos",
|
"search_page_videos": "Videos",
|
||||||
"search_page_view_all_button": "Ver todo",
|
"search_page_view_all_button": "Ver todo",
|
||||||
"search_page_your_activity": "Tu actividad",
|
"search_page_your_activity": "Tu actividad",
|
||||||
"search_page_your_map": "Tu Mapa",
|
"search_page_your_map": "Your Map",
|
||||||
"search_result_page_new_search_hint": "Nueva Busqueda",
|
"search_result_page_new_search_hint": "Nueva Busqueda",
|
||||||
"search_suggestion_list_smart_search_hint_1": "La búsqueda inteligente está habilitada por defecto, para buscar metadatos utiliza esta sintaxis ",
|
"search_suggestion_list_smart_search_hint_1": "La búsqueda inteligente está habilitada por defecto, para buscar metadatos utiliza esta sintaxis ",
|
||||||
"search_suggestion_list_smart_search_hint_2": "m:tu-término-de-búsqueda",
|
"search_suggestion_list_smart_search_hint_2": "m:tu-término-de-búsqueda",
|
||||||
@@ -382,17 +381,17 @@
|
|||||||
"shared_album_activity_remove_title": "Eliminar Actividad",
|
"shared_album_activity_remove_title": "Eliminar Actividad",
|
||||||
"shared_album_activity_setting_subtitle": "Permitir que otros respondan",
|
"shared_album_activity_setting_subtitle": "Permitir que otros respondan",
|
||||||
"shared_album_activity_setting_title": "Comentarios y me gusta",
|
"shared_album_activity_setting_title": "Comentarios y me gusta",
|
||||||
"shared_album_section_people_action_error": "Error dejando/eliminando del album",
|
"shared_album_section_people_action_error": "Error leaving/removing from album",
|
||||||
"shared_album_section_people_action_leave": "Eliminar usuario del album",
|
"shared_album_section_people_action_leave": "Remove user from album",
|
||||||
"shared_album_section_people_action_remove_user": "Eliminar usuario del album",
|
"shared_album_section_people_action_remove_user": "Remove user from album",
|
||||||
"shared_album_section_people_owner_label": "Propietario",
|
"shared_album_section_people_owner_label": "Owner",
|
||||||
"shared_album_section_people_title": "GENTE",
|
"shared_album_section_people_title": "PEOPLE",
|
||||||
"share_dialog_preparing": "Preparando...",
|
"share_dialog_preparing": "Preparando...",
|
||||||
"shared_link_app_bar_title": "Enlaces compartidos",
|
"shared_link_app_bar_title": "Enlaces compartidos",
|
||||||
"shared_link_clipboard_copied_massage": "Copiado al portapapeles",
|
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
||||||
"shared_link_clipboard_text": "Enlace: {}\nContraseña: {}",
|
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
|
||||||
"shared_link_create_app_bar_title": "Crear enlace compartido",
|
"shared_link_create_app_bar_title": "Crear enlace compartido",
|
||||||
"shared_link_create_error": "Error creando el enlace compartido",
|
"shared_link_create_error": "Error while creating shared link",
|
||||||
"shared_link_create_info": "Cualquier persona con el enlace puede ver las fotos seleccionadas",
|
"shared_link_create_info": "Cualquier persona con el enlace puede ver las fotos seleccionadas",
|
||||||
"shared_link_create_submit_button": "Crear enlace",
|
"shared_link_create_submit_button": "Crear enlace",
|
||||||
"shared_link_edit_allow_download": "Permitir descargar a usuarios públicos",
|
"shared_link_edit_allow_download": "Permitir descargar a usuarios públicos",
|
||||||
@@ -402,32 +401,32 @@
|
|||||||
"shared_link_edit_description": "Descripción",
|
"shared_link_edit_description": "Descripción",
|
||||||
"shared_link_edit_description_hint": "Introduce la descripción del enlace",
|
"shared_link_edit_description_hint": "Introduce la descripción del enlace",
|
||||||
"shared_link_edit_expire_after": "Expirar después de",
|
"shared_link_edit_expire_after": "Expirar después de",
|
||||||
"shared_link_edit_expire_after_option_day": "1 día",
|
"shared_link_edit_expire_after_option_day": "1 day",
|
||||||
"shared_link_edit_expire_after_option_days": "{} días",
|
"shared_link_edit_expire_after_option_days": "{} days",
|
||||||
"shared_link_edit_expire_after_option_hour": "1 hora",
|
"shared_link_edit_expire_after_option_hour": "1 hour",
|
||||||
"shared_link_edit_expire_after_option_hours": "{} horas",
|
"shared_link_edit_expire_after_option_hours": "{} hours",
|
||||||
"shared_link_edit_expire_after_option_minute": "1 minuto",
|
"shared_link_edit_expire_after_option_minute": "1 minute",
|
||||||
"shared_link_edit_expire_after_option_minutes": "{} minutos",
|
"shared_link_edit_expire_after_option_minutes": "{} minutes",
|
||||||
"shared_link_edit_expire_after_option_never": "Nunca",
|
"shared_link_edit_expire_after_option_never": "Never",
|
||||||
"shared_link_edit_password": "Contraseña",
|
"shared_link_edit_password": "Contraseña",
|
||||||
"shared_link_edit_password_hint": "Introduce la contraseña del enlace",
|
"shared_link_edit_password_hint": "Introduce la contraseña del enlace",
|
||||||
"shared_link_edit_show_meta": "Mostrar metadatos",
|
"shared_link_edit_show_meta": "Mostrar metadatos",
|
||||||
"shared_link_edit_submit_button": "Actualizar enlace",
|
"shared_link_edit_submit_button": "Actualizar enlace",
|
||||||
"shared_link_empty": "No tienes enlaces compartidos",
|
"shared_link_empty": "No tienes enlaces compartidos",
|
||||||
"shared_link_error_server_url_fetch": "No se puede adquirir la URL del servidor",
|
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
|
||||||
"shared_link_expired": "Caducado",
|
"shared_link_expired": "Expired",
|
||||||
"shared_link_expires_day": "Caduca en {} día",
|
"shared_link_expires_day": "Expires in {} day",
|
||||||
"shared_link_expires_days": "Caduca en {} días",
|
"shared_link_expires_days": "Expires in {} days",
|
||||||
"shared_link_expires_hour": "Caduca en {} hora",
|
"shared_link_expires_hour": "Expires in {} hour",
|
||||||
"shared_link_expires_hours": "Caduca en {} horas",
|
"shared_link_expires_hours": "Expires in {} hours",
|
||||||
"shared_link_expires_minute": "Caduca en {} minuto",
|
"shared_link_expires_minute": "Expires in {} minute",
|
||||||
"shared_link_expires_minutes": "Caduca en {} minutos",
|
"shared_link_expires_minutes": "Caduca en {} minutos",
|
||||||
"shared_link_expires_never": "Caduca ∞",
|
"shared_link_expires_never": "Expires ∞",
|
||||||
"shared_link_expires_second": "Caduca en {} segundo",
|
"shared_link_expires_second": "Expires in {} second",
|
||||||
"shared_link_expires_seconds": "Caduca en {} segundos",
|
"shared_link_expires_seconds": "Caduca en {} segundos",
|
||||||
"shared_link_info_chip_download": "Descargar",
|
"shared_link_info_chip_download": "Download",
|
||||||
"shared_link_info_chip_metadata": "EXIF",
|
"shared_link_info_chip_metadata": "EXIF",
|
||||||
"shared_link_info_chip_upload": "Subir",
|
"shared_link_info_chip_upload": "Upload",
|
||||||
"shared_link_manage_links": "Administrar enlaces compartidos",
|
"shared_link_manage_links": "Administrar enlaces compartidos",
|
||||||
"share_done": "Hecho",
|
"share_done": "Hecho",
|
||||||
"share_invite": "Invitar al álbum",
|
"share_invite": "Invitar al álbum",
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "DETALLES",
|
"exif_bottom_sheet_details": "DETALLES",
|
||||||
"exif_bottom_sheet_location": "UBICACIÓN",
|
"exif_bottom_sheet_location": "UBICACIÓN",
|
||||||
"exif_bottom_sheet_location_add": "Add a location",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
|
"experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
|
||||||
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
|
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
|
||||||
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
|
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "DETALLES",
|
"exif_bottom_sheet_details": "DETALLES",
|
||||||
"exif_bottom_sheet_location": "UBICACIÓN",
|
"exif_bottom_sheet_location": "UBICACIÓN",
|
||||||
"exif_bottom_sheet_location_add": "Add a location",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
|
"experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
|
||||||
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
|
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
|
||||||
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
|
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "TIEDOT",
|
"exif_bottom_sheet_details": "TIEDOT",
|
||||||
"exif_bottom_sheet_location": "SIJAINTI",
|
"exif_bottom_sheet_location": "SIJAINTI",
|
||||||
"exif_bottom_sheet_location_add": "Lisää sijainti",
|
"exif_bottom_sheet_location_add": "Lisää sijainti",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Työn alla",
|
"experimental_settings_new_asset_list_subtitle": "Työn alla",
|
||||||
"experimental_settings_new_asset_list_title": "Ota käyttöön kokeellinen kuvaruudukko",
|
"experimental_settings_new_asset_list_title": "Ota käyttöön kokeellinen kuvaruudukko",
|
||||||
"experimental_settings_subtitle": "Käyttö omalla vastuulla!",
|
"experimental_settings_subtitle": "Käyttö omalla vastuulla!",
|
||||||
@@ -200,7 +199,7 @@
|
|||||||
"home_page_archive_err_partner": "Kumppanin kohteita ei voi arkistoida. Hypätään yli",
|
"home_page_archive_err_partner": "Kumppanin kohteita ei voi arkistoida. Hypätään yli",
|
||||||
"home_page_building_timeline": "Rakennetaan aikajanaa",
|
"home_page_building_timeline": "Rakennetaan aikajanaa",
|
||||||
"home_page_delete_err_partner": "Kumppanin kohteita ei voi poistaa.Hypätään yli",
|
"home_page_delete_err_partner": "Kumppanin kohteita ei voi poistaa.Hypätään yli",
|
||||||
"home_page_delete_remote_err_local": "Paikallisia kohteita etäkohdevalintojen joukossa, ohitetaan",
|
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
|
||||||
"home_page_favorite_err_local": "Paikallisten kohteiden lisääminen suosikkeihin ei ole mahdollista, ohitetaan",
|
"home_page_favorite_err_local": "Paikallisten kohteiden lisääminen suosikkeihin ei ole mahdollista, ohitetaan",
|
||||||
"home_page_favorite_err_partner": "Kumppanin kohteita ei voi vielä merkitä suosikiksi. Hypätään yli",
|
"home_page_favorite_err_partner": "Kumppanin kohteita ei voi vielä merkitä suosikiksi. Hypätään yli",
|
||||||
"home_page_first_time_notice": "Jos käytät sovellusta ensimmäistä kertaa, muista valita varmuuskopioitavat albumi(t), jotta aikajanalla voi olla kuvia ja videoita.",
|
"home_page_first_time_notice": "Jos käytät sovellusta ensimmäistä kertaa, muista valita varmuuskopioitavat albumi(t), jotta aikajanalla voi olla kuvia ja videoita.",
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "DÉTAILS",
|
"exif_bottom_sheet_details": "DÉTAILS",
|
||||||
"exif_bottom_sheet_location": "LOCALISATION",
|
"exif_bottom_sheet_location": "LOCALISATION",
|
||||||
"exif_bottom_sheet_location_add": "Add a location",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "En cours de développement",
|
"experimental_settings_new_asset_list_subtitle": "En cours de développement",
|
||||||
"experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale",
|
"experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale",
|
||||||
"experimental_settings_subtitle": "Utilisez à vos dépends !",
|
"experimental_settings_subtitle": "Utilisez à vos dépends !",
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "DETAILS",
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
"exif_bottom_sheet_location": "LOCATION",
|
"exif_bottom_sheet_location": "LOCATION",
|
||||||
"exif_bottom_sheet_location_add": "Add a location",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||||
"experimental_settings_subtitle": "Use at your own risk!",
|
"experimental_settings_subtitle": "Use at your own risk!",
|
||||||
|
|||||||
@@ -1,74 +1,74 @@
|
|||||||
{
|
{
|
||||||
"action_common_cancel": "Mégsem",
|
"action_common_cancel": "Cancel",
|
||||||
"action_common_update": "Frissít",
|
"action_common_update": "Update",
|
||||||
"add_to_album_bottom_sheet_added": "Hozzáadva a(z) {album} nevű albumhoz",
|
"add_to_album_bottom_sheet_added": "Hozzáadva a(z) {album} nevű albumhoz",
|
||||||
"add_to_album_bottom_sheet_already_exists": "Már eleme a(z) {album} nevű albumnak",
|
"add_to_album_bottom_sheet_already_exists": "Már eleme a(z) {album} nevű albumnak",
|
||||||
"advanced_settings_log_level_title": "Naplózás szintje: {}",
|
"advanced_settings_log_level_title": "Log level: {}",
|
||||||
"advanced_settings_prefer_remote_subtitle": "Néhány eszköz fájdalmasan lassan tölti be az eszközön lévő elemeket. Ezzel a beállítással inkább a távoli képeket töltjük be helyette.",
|
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
|
||||||
"advanced_settings_prefer_remote_title": "Távoli képek preferálása",
|
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
||||||
"advanced_settings_self_signed_ssl_subtitle": "SSL tanúsítvány ellenőrzésének kihagyása a szerver végponthoz. Ehhez saját aláírt tanúsítványok szükségesek.",
|
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
||||||
"advanced_settings_self_signed_ssl_title": "Saját aláírt SSL tanúsítványok engedélyezése",
|
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
||||||
"advanced_settings_tile_subtitle": "Haladó felhasználói beállítások",
|
"advanced_settings_tile_subtitle": "Haladó felhasználói beállítások",
|
||||||
"advanced_settings_tile_title": "Haladó",
|
"advanced_settings_tile_title": "Haladó",
|
||||||
"advanced_settings_troubleshooting_subtitle": "További funkciók engedélyezése hibaelhárítás céljából",
|
"advanced_settings_troubleshooting_subtitle": "További funkciók engedélyezése hibaelhárítás céljából",
|
||||||
"advanced_settings_troubleshooting_title": "Hibaelhárítás",
|
"advanced_settings_troubleshooting_title": "Hibaelhárítás",
|
||||||
"album_info_card_backup_album_excluded": "KIZÁRVA",
|
"album_info_card_backup_album_excluded": "EXCLUDED",
|
||||||
"album_info_card_backup_album_included": "BELEÉRTVE",
|
"album_info_card_backup_album_included": "INCLUDED",
|
||||||
"album_thumbnail_card_item": "1 elem",
|
"album_thumbnail_card_item": "1 elem",
|
||||||
"album_thumbnail_card_items": "{} elem",
|
"album_thumbnail_card_items": "{} elem",
|
||||||
"album_thumbnail_card_shared": "· Megosztott",
|
"album_thumbnail_card_shared": "· Megosztott",
|
||||||
"album_thumbnail_owned": "Tulajdonos",
|
"album_thumbnail_owned": "Tulajdonos",
|
||||||
"album_thumbnail_shared_by": "Megosztotta: {}",
|
"album_thumbnail_shared_by": "Megosztotta: {}",
|
||||||
"album_viewer_appbar_share_delete": "Album törlése",
|
"album_viewer_appbar_share_delete": "Album törlése",
|
||||||
"album_viewer_appbar_share_err_delete": "Nem sikerült törölni az albumot",
|
"album_viewer_appbar_share_err_delete": "Hiba az album törlése közben",
|
||||||
"album_viewer_appbar_share_err_leave": "Nem sikerült kilépni az albumból",
|
"album_viewer_appbar_share_err_leave": "Hiba az albumból való kilépés közben",
|
||||||
"album_viewer_appbar_share_err_remove": "Néhány elemet nem sikerült törölni az albumból",
|
"album_viewer_appbar_share_err_remove": "Hiba az elemek törlése közben",
|
||||||
"album_viewer_appbar_share_err_title": "Nem sikerült átnevezni az albumot",
|
"album_viewer_appbar_share_err_title": "Hiba az album átnevezése közben",
|
||||||
"album_viewer_appbar_share_leave": "Kilépés az albumból",
|
"album_viewer_appbar_share_leave": "Kilépés az albumból",
|
||||||
"album_viewer_appbar_share_remove": "Eltávolítás az albumból",
|
"album_viewer_appbar_share_remove": "Törlés az albumból",
|
||||||
"album_viewer_appbar_share_to": "Megosztás Ide",
|
"album_viewer_appbar_share_to": "Share To",
|
||||||
"album_viewer_page_share_add_users": "Felhasználók hozzáadása",
|
"album_viewer_page_share_add_users": "Felhasználók hozzáadása",
|
||||||
"all_people_page_title": "Emberek",
|
"all_people_page_title": "Emberek",
|
||||||
"all_videos_page_title": "Videók",
|
"all_videos_page_title": "Videók",
|
||||||
"app_bar_signout_dialog_content": "Biztos, hogy ki szeretnél jelentkezni?",
|
"app_bar_signout_dialog_content": "Are you sure you want to sign out?",
|
||||||
"app_bar_signout_dialog_ok": "Igen",
|
"app_bar_signout_dialog_ok": "Yes",
|
||||||
"app_bar_signout_dialog_title": "Kijelentkezés",
|
"app_bar_signout_dialog_title": "Sign out",
|
||||||
"archive_page_no_archived_assets": "Nem található archivált elem",
|
"archive_page_no_archived_assets": "Nem található archivált média",
|
||||||
"archive_page_title": "Archívum ({})",
|
"archive_page_title": "Archívum ({})",
|
||||||
"asset_action_delete_err_read_only": "Nem sikerült törölni a csak-olvasható elem(ek)et, így ezeket átugorjuk",
|
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
|
||||||
"asset_action_share_err_offline": "Nem sikerült betölteni az offline elem(ek)et, így ezeket kihagyjuk",
|
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||||
"asset_list_layout_settings_dynamic_layout_title": "Dinamikus elrendezés",
|
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
|
||||||
"asset_list_layout_settings_group_automatically": "Automatikus",
|
"asset_list_layout_settings_group_automatically": "Automatikus",
|
||||||
"asset_list_layout_settings_group_by": "Elemek csoportosítása",
|
"asset_list_layout_settings_group_by": "Group assets by",
|
||||||
"asset_list_layout_settings_group_by_month": "hónapok szerint",
|
"asset_list_layout_settings_group_by_month": "Hónap",
|
||||||
"asset_list_layout_settings_group_by_month_day": "hónap és nap szerint",
|
"asset_list_layout_settings_group_by_month_day": "Hónap + nap",
|
||||||
"asset_list_settings_subtitle": "Fotórács elrendezése",
|
"asset_list_settings_subtitle": "Photo grid layout settings",
|
||||||
"asset_list_settings_title": "Fotórács",
|
"asset_list_settings_title": "Photo Grid",
|
||||||
"backup_album_selection_page_albums_device": "Ezen az eszközön lévő albumok ({})",
|
"backup_album_selection_page_albums_device": "Az eszközön lévő albumok ({})",
|
||||||
"backup_album_selection_page_albums_tap": "Koppincs a hozzáadáshoz, duplán koppincs az eltávolításhoz",
|
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
||||||
"backup_album_selection_page_assets_scatter": "Egy elem több albumban is lehet. Ezért a mentéshez albumokat lehet hozzáadni vagy azokat a mentésből kihagyni.",
|
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||||
"backup_album_selection_page_select_albums": "Válassz albumokat",
|
"backup_album_selection_page_select_albums": "Albumok kiválasztása",
|
||||||
"backup_album_selection_page_selection_info": "Összegzés",
|
"backup_album_selection_page_selection_info": "Selection Info",
|
||||||
"backup_album_selection_page_total_assets": "Összes egyedi elem",
|
"backup_album_selection_page_total_assets": "Összes egyedi elem",
|
||||||
"backup_all": "Összes",
|
"backup_all": "Összes",
|
||||||
"backup_background_service_backup_failed_message": "HIba a mentés közben. Újrapróbálkozás...",
|
"backup_background_service_backup_failed_message": "HIba a mentés közben. Újrapróbálkozás...",
|
||||||
"backup_background_service_connection_failed_message": "HIba a szerverhez való csatlakozás közben. Újrapróbálkozás...",
|
"backup_background_service_connection_failed_message": "HIba a szerverhez való csatlakozás közben. Újrapróbálkozás...",
|
||||||
"backup_background_service_current_upload_notification": "Feltöltés {}",
|
"backup_background_service_current_upload_notification": "Feltöltés {}",
|
||||||
"backup_background_service_default_notification": "Új elemek keresése...",
|
"backup_background_service_default_notification": "Keresés új elemek után...",
|
||||||
"backup_background_service_error_title": "Hiba mentés közben",
|
"backup_background_service_error_title": "Hiba mentés közben",
|
||||||
"backup_background_service_in_progress_notification": "Elemek mentés alatt..",
|
"backup_background_service_in_progress_notification": "Elemek mentés alatt..",
|
||||||
"backup_background_service_upload_failure_notification": "Hiba feltöltés közben {}",
|
"backup_background_service_upload_failure_notification": "Hiba feltöltés közben {}",
|
||||||
"backup_controller_page_albums": "Albumok Mentése",
|
"backup_controller_page_albums": "Albumok mentése",
|
||||||
"backup_controller_page_background_app_refresh_disabled_content": "Engedélyezd a háttérben történő frissítést a Beállítások > Általános > Háttérben Frissítés menüpontban.",
|
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
|
||||||
"backup_controller_page_background_app_refresh_disabled_title": "Háttérben frissítés kikapcsolva",
|
"backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled",
|
||||||
"backup_controller_page_background_app_refresh_enable_button_text": "Beállítások megnyitása",
|
"backup_controller_page_background_app_refresh_enable_button_text": "Beállítások megnyitása",
|
||||||
"backup_controller_page_background_battery_info_link": "Mutasd meg hogyan",
|
"backup_controller_page_background_battery_info_link": "Mutasd meg hogyan",
|
||||||
"backup_controller_page_background_battery_info_message": "A sikeres háttérben történő mentéshez kérjük, tiltsd le az Immich akkumulátor optimalizálását.\n\nMivel ezt a különféle eszközökön máshogy kell, ezért kérjük, az eszközöd gyártójától tudd meg, hogyan kell.",
|
"backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.",
|
||||||
"backup_controller_page_background_battery_info_ok": "OK",
|
"backup_controller_page_background_battery_info_ok": "OK",
|
||||||
"backup_controller_page_background_battery_info_title": "Akkumulátor optimalizálás",
|
"backup_controller_page_background_battery_info_title": "Akkumulátoroptimalizálás",
|
||||||
"backup_controller_page_background_charging": "Csak töltés közben",
|
"backup_controller_page_background_charging": "Csak töltés közben",
|
||||||
"backup_controller_page_background_configure_error": "Nem sikerült beállítani a háttér szolgáltatást",
|
"backup_controller_page_background_configure_error": "Failed to configure the background service",
|
||||||
"backup_controller_page_background_delay": "Új elemek mentésének késleltetése: {}",
|
"backup_controller_page_background_delay": "Delay new assets backup: {}",
|
||||||
"backup_controller_page_background_description": "Kapcsold be a háttérfolyamatot, hogy automatikusan mentsen elemeket az applikáció megnyitása nélkül",
|
"backup_controller_page_background_description": "Kapcsold be a háttérfolyamatot, hogy automatikusan mentsen elemeket az applikáció megnyitása nélkül",
|
||||||
"backup_controller_page_background_is_off": "Automatikus mentés a háttérben ki van kapcsolva",
|
"backup_controller_page_background_is_off": "Automatikus mentés a háttérben ki van kapcsolva",
|
||||||
"backup_controller_page_background_is_on": "Automatikus mentés a háttérben bekapcsolva",
|
"backup_controller_page_background_is_on": "Automatikus mentés a háttérben bekapcsolva",
|
||||||
@@ -78,63 +78,63 @@
|
|||||||
"backup_controller_page_backup": "Mentés",
|
"backup_controller_page_backup": "Mentés",
|
||||||
"backup_controller_page_backup_selected": "Kiválasztva:",
|
"backup_controller_page_backup_selected": "Kiválasztva:",
|
||||||
"backup_controller_page_backup_sub": "Mentett fotók és videók",
|
"backup_controller_page_backup_sub": "Mentett fotók és videók",
|
||||||
"backup_controller_page_cancel": "Mégsem",
|
"backup_controller_page_cancel": "Megszakít",
|
||||||
"backup_controller_page_created": "Létrehozva: {}",
|
"backup_controller_page_created": "Létrehozva: {}",
|
||||||
"backup_controller_page_desc_backup": "Ha engedélyezed az előtérben mentést, akkor az új elemek automatikusan feltöltődnek a szerverre, amikor megyitod az alkalmazást.",
|
"backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.",
|
||||||
"backup_controller_page_excluded": "Kivéve:",
|
"backup_controller_page_excluded": "Kivéve:",
|
||||||
"backup_controller_page_failed": "Sikertelen ({})",
|
"backup_controller_page_failed": "Sikertelen ({})",
|
||||||
"backup_controller_page_filename": "Fájlnév: {}[{}]",
|
"backup_controller_page_filename": "Fájlnév: {}[{}]",
|
||||||
"backup_controller_page_id": "Azonosító: {}",
|
"backup_controller_page_id": "Azonosító: {}",
|
||||||
"backup_controller_page_info": "Mentésinformációk",
|
"backup_controller_page_info": "Mentésinformációk",
|
||||||
"backup_controller_page_none_selected": "Egy sincs kiválasztva",
|
"backup_controller_page_none_selected": "Egy sincs kiválasztva",
|
||||||
"backup_controller_page_remainder": "Hátralévő",
|
"backup_controller_page_remainder": "Maradék",
|
||||||
"backup_controller_page_remainder_sub": "Hátralévő fotók és videók a kijelöltek közül",
|
"backup_controller_page_remainder_sub": "Hátralévő fotók és videók a kijelöltek közül",
|
||||||
"backup_controller_page_select": "Kiválaszt",
|
"backup_controller_page_select": "Kiválaszt",
|
||||||
"backup_controller_page_server_storage": "Szerver Tárhely",
|
"backup_controller_page_server_storage": "Szerver tárhely",
|
||||||
"backup_controller_page_start_backup": "Mentés Elindítása",
|
"backup_controller_page_start_backup": "Mentés elindítása",
|
||||||
"backup_controller_page_status_off": "Automatikus mentés az előtérben kikapcsolva",
|
"backup_controller_page_status_off": "Autoatikus mentés az előtérben kikapcsolva",
|
||||||
"backup_controller_page_status_on": "Automatikus mentés az előtérben bekapcsolva",
|
"backup_controller_page_status_on": "Autoatikus mentés az előtérben bekapcsolva",
|
||||||
"backup_controller_page_storage_format": "{} / {} felhasználva",
|
"backup_controller_page_storage_format": "{} / {} felhasználva",
|
||||||
"backup_controller_page_to_backup": "Mentésre kijelölt albumok",
|
"backup_controller_page_to_backup": "Albumok amiket mentesz",
|
||||||
"backup_controller_page_total": "Összesen",
|
"backup_controller_page_total": "Összes",
|
||||||
"backup_controller_page_total_sub": "Minden egyedi fotó és videó a kijelölt albumokból",
|
"backup_controller_page_total_sub": "Minden egyedi fotó és videó a kijelölt albumokból",
|
||||||
"backup_controller_page_turn_off": "Előtérben mentés kikapcsolása",
|
"backup_controller_page_turn_off": "Turn off foreground backup",
|
||||||
"backup_controller_page_turn_on": "Előtérben mentés bekapcsolása",
|
"backup_controller_page_turn_on": "Turn on foreground backup",
|
||||||
"backup_controller_page_uploading_file_info": "Fájl információk feltöltése",
|
"backup_controller_page_uploading_file_info": "Uploading file info",
|
||||||
"backup_err_only_album": "Az utolsó albumot nem tudod törölni",
|
"backup_err_only_album": "Az utolsó albumot nem tudod törölni",
|
||||||
"backup_info_card_assets": "elemek",
|
"backup_info_card_assets": "elemek",
|
||||||
"backup_manual_cancelled": "Megszakítva",
|
"backup_manual_cancelled": "Megszakítva",
|
||||||
"backup_manual_failed": "Sikertelen",
|
"backup_manual_failed": "Failed",
|
||||||
"backup_manual_in_progress": "Feltöltés már folyamatban. Próbáld meg később",
|
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
|
||||||
"backup_manual_success": "Sikeres",
|
"backup_manual_success": "Sikeres",
|
||||||
"backup_manual_title": "Feltöltés állapota",
|
"backup_manual_title": "Upload status",
|
||||||
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
|
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
|
||||||
"cache_settings_clear_cache_button": "Gyorsítótár kiürítése",
|
"cache_settings_clear_cache_button": "Gyorsítótár törlése",
|
||||||
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
|
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
|
||||||
"cache_settings_duplicated_assets_clear_button": "KIÜRÍT",
|
"cache_settings_duplicated_assets_clear_button": "CLEAR",
|
||||||
"cache_settings_duplicated_assets_subtitle": "Fotók és videók, amiket az alkalmazás fekete listára tett",
|
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
|
||||||
"cache_settings_duplicated_assets_title": "Duplikált Elemek ({})",
|
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
|
||||||
"cache_settings_image_cache_size": "Kép gyorsítótár mérete ({} elem)",
|
"cache_settings_image_cache_size": "Image cache size ({} assets)",
|
||||||
"cache_settings_statistics_album": "Mappa bélyegképei",
|
"cache_settings_statistics_album": "Library thumbnails",
|
||||||
"cache_settings_statistics_assets": "{} elem ({})",
|
"cache_settings_statistics_assets": "{} assets ({})",
|
||||||
"cache_settings_statistics_full": "Teljes méretű képek",
|
"cache_settings_statistics_full": "Teljes képek",
|
||||||
"cache_settings_statistics_shared": "Megosztott album bélyegképei",
|
"cache_settings_statistics_shared": "Shared album thumbnails",
|
||||||
"cache_settings_statistics_thumbnail": "Bélyegképek",
|
"cache_settings_statistics_thumbnail": "Előnézeti képek",
|
||||||
"cache_settings_statistics_title": "Gyorsítótár használata",
|
"cache_settings_statistics_title": "Gyorsítótár által használt terület",
|
||||||
"cache_settings_subtitle": "Az Immich mobilalkalmazás gyorsítótár viselkedésének beállítása",
|
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
|
||||||
"cache_settings_thumbnail_size": "Bélyegkép gyorsítótár mérete ({} elem)",
|
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
|
||||||
"cache_settings_tile_subtitle": "Helyi tárhely viselkedésének beállítása",
|
"cache_settings_tile_subtitle": "Control the local storage behaviour",
|
||||||
"cache_settings_tile_title": "Helyi Tárhely",
|
"cache_settings_tile_title": "Local Storage",
|
||||||
"cache_settings_title": "Gyorsítótár Beállítások",
|
"cache_settings_title": "Gyorsítótár beállítások",
|
||||||
"change_password_form_confirm_password": "Jelszó Megerősítése",
|
"change_password_form_confirm_password": "Jelszó Megerősítése",
|
||||||
"change_password_form_description": "Kedves {name}!\n\nMost jelentkezel be először a rendszerbe vagy más okból szükséges a jelszavad meváltoztatása. Kérjük, add meg új jelszavad.",
|
"change_password_form_description": "Kedves {name}!\n\nMost jelentkezel be először a rendszerbe vagy más okból szükséfes a jelszavad meváltoztatása. Kérjük, add meg új jelszavad.",
|
||||||
"change_password_form_new_password": "Új Jelszó",
|
"change_password_form_new_password": "Új Jelszó",
|
||||||
"change_password_form_password_mismatch": "A két beírt jelszó nem egyezik",
|
"change_password_form_password_mismatch": "A két beírt jelszó nem egyezik",
|
||||||
"change_password_form_reenter_new_password": "Jelszó (még egyszer)",
|
"change_password_form_reenter_new_password": "Jelszó (még egyszer)",
|
||||||
"common_add_to_album": "Albumhoz ad",
|
"common_add_to_album": "Albumhoz ad",
|
||||||
"common_change_password": "Jelszócsere",
|
"common_change_password": "Jelszócsere",
|
||||||
"common_create_new_album": "Új album létrehozása",
|
"common_create_new_album": "Új album létrehozása",
|
||||||
"common_server_error": "Kérjük, ellenőrizd a hálózati kapcsolatot, gondoskodj róla, hogy a szerver elérhető legyen, valamint az alkalmazás és a szerver kompatibilis verziójú legyen.",
|
"common_server_error": "Kérjük, ellenőrizd a hálózati kapcsolatot, gondoskodj róla, hogy a szerver elérhető legyen, valamint az app és a szerver kompatibilis verziójú legyen.",
|
||||||
"common_shared": "Megosztva",
|
"common_shared": "Megosztva",
|
||||||
"control_bottom_app_bar_add_to_album": "Hozzáadás az albumhoz",
|
"control_bottom_app_bar_add_to_album": "Hozzáadás az albumhoz",
|
||||||
"control_bottom_app_bar_album_info": "{} elem",
|
"control_bottom_app_bar_album_info": "{} elem",
|
||||||
@@ -142,23 +142,23 @@
|
|||||||
"control_bottom_app_bar_archive": "Archivál",
|
"control_bottom_app_bar_archive": "Archivál",
|
||||||
"control_bottom_app_bar_create_new_album": "Album létrehozása",
|
"control_bottom_app_bar_create_new_album": "Album létrehozása",
|
||||||
"control_bottom_app_bar_delete": "Törlés",
|
"control_bottom_app_bar_delete": "Törlés",
|
||||||
"control_bottom_app_bar_delete_from_immich": "Törlés az Immich-ből",
|
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||||
"control_bottom_app_bar_delete_from_local": "Törlés az eszközről",
|
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||||
"control_bottom_app_bar_edit_location": "Hely Módosítása",
|
"control_bottom_app_bar_edit_location": "Edit Location",
|
||||||
"control_bottom_app_bar_edit_time": "Dátum és Idő Módosítása",
|
"control_bottom_app_bar_edit_time": "Edit Date & Time",
|
||||||
"control_bottom_app_bar_favorite": "Kedvenc",
|
"control_bottom_app_bar_favorite": "Kedvenc",
|
||||||
"control_bottom_app_bar_share": "Megosztás",
|
"control_bottom_app_bar_share": "Megosztás",
|
||||||
"control_bottom_app_bar_share_to": "Megosztás Ide",
|
"control_bottom_app_bar_share_to": "Share To",
|
||||||
"control_bottom_app_bar_stack": "Stack",
|
"control_bottom_app_bar_stack": "Stack",
|
||||||
"control_bottom_app_bar_trash_from_immich": "Lomtárba Helyez",
|
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
|
||||||
"control_bottom_app_bar_unarchive": "Nem Archivált",
|
"control_bottom_app_bar_unarchive": "Archiválás megszüntetése",
|
||||||
"control_bottom_app_bar_unfavorite": "Nem Kedvenc",
|
"control_bottom_app_bar_unfavorite": "Unfavorite",
|
||||||
"control_bottom_app_bar_upload": "Feltöltés",
|
"control_bottom_app_bar_upload": "Upload",
|
||||||
"create_album_page_untitled": "Névtelen",
|
"create_album_page_untitled": "Névtelen",
|
||||||
"create_shared_album_page_create": "Létrehoz",
|
"create_shared_album_page_create": "Létrehoz",
|
||||||
"create_shared_album_page_share": "Megosztás",
|
"create_shared_album_page_share": "Megosztás",
|
||||||
"create_shared_album_page_share_add_assets": "ELEMEK HOZZÁADÁSA",
|
"create_shared_album_page_share_add_assets": "ELEMEK HOZZÁADÁSA",
|
||||||
"create_shared_album_page_share_select_photos": "Fotók választása",
|
"create_shared_album_page_share_select_photos": "Fotók kiválasztása",
|
||||||
"curated_location_page_title": "Helyek",
|
"curated_location_page_title": "Helyek",
|
||||||
"curated_object_page_title": "Dolgok",
|
"curated_object_page_title": "Dolgok",
|
||||||
"daily_title_text_date": "E, MMM dd",
|
"daily_title_text_date": "E, MMM dd",
|
||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "RÉSZLETEK",
|
"exif_bottom_sheet_details": "RÉSZLETEK",
|
||||||
"exif_bottom_sheet_location": "HELYSZÍN",
|
"exif_bottom_sheet_location": "HELYSZÍN",
|
||||||
"exif_bottom_sheet_location_add": "Add a location",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Fejlesztés alatt",
|
"experimental_settings_new_asset_list_subtitle": "Fejlesztés alatt",
|
||||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||||
"experimental_settings_subtitle": "Csak saját felelősségre használd",
|
"experimental_settings_subtitle": "Csak saját felelősségre használd",
|
||||||
@@ -231,10 +230,10 @@
|
|||||||
"login_disabled": "A bejelentkezés letiltva",
|
"login_disabled": "A bejelentkezés letiltva",
|
||||||
"login_form_api_exception": "API hiba. Kérljük, ellenőrid a szerver címét, majd próbáld újra.",
|
"login_form_api_exception": "API hiba. Kérljük, ellenőrid a szerver címét, majd próbáld újra.",
|
||||||
"login_form_back_button_text": "Back",
|
"login_form_back_button_text": "Back",
|
||||||
"login_form_button_text": "Bejelentkezés",
|
"login_form_button_text": "Belépés",
|
||||||
"login_form_email_hint": "email@cimed.hu",
|
"login_form_email_hint": "teemailed@email.com",
|
||||||
"login_form_endpoint_hint": "http://szerver-címe:port/api",
|
"login_form_endpoint_hint": "http://szerver-címe:port/api",
|
||||||
"login_form_endpoint_url": "Szerver címe",
|
"login_form_endpoint_url": "Kiszolgáló végpont címe",
|
||||||
"login_form_err_http": "Kérem, adjon meg egy http:// vagy https:// címet",
|
"login_form_err_http": "Kérem, adjon meg egy http:// vagy https:// címet",
|
||||||
"login_form_err_invalid_email": "Érvénytelen email cím",
|
"login_form_err_invalid_email": "Érvénytelen email cím",
|
||||||
"login_form_err_invalid_url": "Érvénytelen cím",
|
"login_form_err_invalid_url": "Érvénytelen cím",
|
||||||
@@ -347,7 +346,7 @@
|
|||||||
"search_suggestion_list_smart_search_hint_1": "Az intelligens keresés alapértelmezetten be van kapcsolva, metaadatokat így kereshetsz",
|
"search_suggestion_list_smart_search_hint_1": "Az intelligens keresés alapértelmezetten be van kapcsolva, metaadatokat így kereshetsz",
|
||||||
"search_suggestion_list_smart_search_hint_2": "m:keresési-kifejezés",
|
"search_suggestion_list_smart_search_hint_2": "m:keresési-kifejezés",
|
||||||
"select_additional_user_for_sharing_page_suggestions": "Javaslatok",
|
"select_additional_user_for_sharing_page_suggestions": "Javaslatok",
|
||||||
"select_user_for_sharing_page_err_album": "Nem sikerült létrehozni az albumot",
|
"select_user_for_sharing_page_err_album": "Hiba az album létrehozása közben",
|
||||||
"select_user_for_sharing_page_share_suggestions": "Javaslatok",
|
"select_user_for_sharing_page_share_suggestions": "Javaslatok",
|
||||||
"server_info_box_app_version": "Alkalmazás Verzió",
|
"server_info_box_app_version": "Alkalmazás Verzió",
|
||||||
"server_info_box_latest_release": "Latest Version",
|
"server_info_box_latest_release": "Latest Version",
|
||||||
@@ -374,7 +373,7 @@
|
|||||||
"settings_require_restart": "Kérlek indítsd újra az Immich-et hogy alkalmazd ezt a beállítást",
|
"settings_require_restart": "Kérlek indítsd újra az Immich-et hogy alkalmazd ezt a beállítást",
|
||||||
"share_add": "Hozzáadás",
|
"share_add": "Hozzáadás",
|
||||||
"share_add_photos": "Fotók hozzáadása",
|
"share_add_photos": "Fotók hozzáadása",
|
||||||
"share_add_title": "Album neve",
|
"share_add_title": "Cím hozzáadása",
|
||||||
"share_create_album": "Album létrehozása",
|
"share_create_album": "Album létrehozása",
|
||||||
"shared_album_activities_input_disable": "Comment is disabled",
|
"shared_album_activities_input_disable": "Comment is disabled",
|
||||||
"shared_album_activities_input_hint": "Say something",
|
"shared_album_activities_input_hint": "Say something",
|
||||||
@@ -432,11 +431,11 @@
|
|||||||
"share_done": "Done",
|
"share_done": "Done",
|
||||||
"share_invite": "Meghívás az albumba",
|
"share_invite": "Meghívás az albumba",
|
||||||
"sharing_page_album": "Megosztott albumok",
|
"sharing_page_album": "Megosztott albumok",
|
||||||
"sharing_page_description": "Megosztott albumok létrehozásával fényképeket és videókatoszthatsz meg a hálózatodban lévő emberekkel.",
|
"sharing_page_description": "Hozzon létre megosztott albumokat, hogy megoszthasson fényképeket és videókat a hálózatában lévő emberekkel.",
|
||||||
"sharing_page_empty_list": "ÜRES LISTA",
|
"sharing_page_empty_list": "ÜRES LISTA",
|
||||||
"sharing_silver_appbar_create_shared_album": "Megosztott album létrehozása",
|
"sharing_silver_appbar_create_shared_album": "Megosztott album létrehozása",
|
||||||
"sharing_silver_appbar_shared_links": "Shared links",
|
"sharing_silver_appbar_shared_links": "Shared links",
|
||||||
"sharing_silver_appbar_share_partner": "Megosztás partnerrel",
|
"sharing_silver_appbar_share_partner": "Megosztás másokkal",
|
||||||
"tab_controller_nav_library": "Könyvtár",
|
"tab_controller_nav_library": "Könyvtár",
|
||||||
"tab_controller_nav_photos": "Képek",
|
"tab_controller_nav_photos": "Képek",
|
||||||
"tab_controller_nav_search": "Keresés",
|
"tab_controller_nav_search": "Keresés",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"action_common_cancel": "Annulla",
|
"action_common_cancel": "Cancel",
|
||||||
"action_common_update": "Aggiorna",
|
"action_common_update": "Update",
|
||||||
"add_to_album_bottom_sheet_added": "Aggiunto in {album}",
|
"add_to_album_bottom_sheet_added": "Aggiunto in {album}",
|
||||||
"add_to_album_bottom_sheet_already_exists": "Già presente in {album}",
|
"add_to_album_bottom_sheet_already_exists": "Già presente in {album}",
|
||||||
"advanced_settings_log_level_title": "Livello log: {}",
|
"advanced_settings_log_level_title": "Log level: {}",
|
||||||
"advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini dal dispositivo. Attivare questa impostazione per caricare invece le immagini remote.",
|
"advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini dal dispositivo. Attivare questa impostazione per caricare invece le immagini remote.",
|
||||||
"advanced_settings_prefer_remote_title": "Preferisci immagini remote.",
|
"advanced_settings_prefer_remote_title": "Preferisci immagini remote.",
|
||||||
"advanced_settings_self_signed_ssl_subtitle": "Salta la verifica dei certificati SSL del server. Richiesto con l'uso di certificati self-signed.",
|
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
||||||
"advanced_settings_self_signed_ssl_title": "Consenti certificati SSL self-signed",
|
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
||||||
"advanced_settings_tile_subtitle": "Impostazioni aggiuntive utenti",
|
"advanced_settings_tile_subtitle": "Impostazioni aggiuntive utenti",
|
||||||
"advanced_settings_tile_title": "Avanzato",
|
"advanced_settings_tile_title": "Avanzato",
|
||||||
"advanced_settings_troubleshooting_subtitle": "Attiva funzioni addizionali per la risoluzione dei problemi",
|
"advanced_settings_troubleshooting_subtitle": "Attiva funzioni addizionali per la risoluzione dei problemi",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"app_bar_signout_dialog_title": "Disconnetti",
|
"app_bar_signout_dialog_title": "Disconnetti",
|
||||||
"archive_page_no_archived_assets": "Nessuna oggetto archiviato",
|
"archive_page_no_archived_assets": "Nessuna oggetto archiviato",
|
||||||
"archive_page_title": "Archivia ({})",
|
"archive_page_title": "Archivia ({})",
|
||||||
"asset_action_delete_err_read_only": "Non posso eliminare degli elementi in sola lettura, ignorato",
|
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
|
||||||
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||||
"asset_list_layout_settings_dynamic_layout_title": "Layout dinamico",
|
"asset_list_layout_settings_dynamic_layout_title": "Layout dinamico",
|
||||||
"asset_list_layout_settings_group_automatically": "Automatico",
|
"asset_list_layout_settings_group_automatically": "Automatico",
|
||||||
@@ -111,9 +111,9 @@
|
|||||||
"cache_settings_album_thumbnails": "Anteprime pagine librerie ({} assets)",
|
"cache_settings_album_thumbnails": "Anteprime pagine librerie ({} assets)",
|
||||||
"cache_settings_clear_cache_button": "Cancella cache",
|
"cache_settings_clear_cache_button": "Cancella cache",
|
||||||
"cache_settings_clear_cache_button_title": "Cancella la cache dell'app. Questo impatterà significativamente le prestazioni dell''app fino a quando la cache non sarà rigenerata.",
|
"cache_settings_clear_cache_button_title": "Cancella la cache dell'app. Questo impatterà significativamente le prestazioni dell''app fino a quando la cache non sarà rigenerata.",
|
||||||
"cache_settings_duplicated_assets_clear_button": "ELIMINA",
|
"cache_settings_duplicated_assets_clear_button": "CLEAR",
|
||||||
"cache_settings_duplicated_assets_subtitle": "Foto e video che sono nella black list dell'applicazione",
|
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
|
||||||
"cache_settings_duplicated_assets_title": "Elementi duplicati ({})",
|
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
|
||||||
"cache_settings_image_cache_size": "Dimensione cache delle foto ({} assets)",
|
"cache_settings_image_cache_size": "Dimensione cache delle foto ({} assets)",
|
||||||
"cache_settings_statistics_album": "Anteprime librerie",
|
"cache_settings_statistics_album": "Anteprime librerie",
|
||||||
"cache_settings_statistics_assets": "{} contenuti ({})",
|
"cache_settings_statistics_assets": "{} contenuti ({})",
|
||||||
@@ -142,15 +142,15 @@
|
|||||||
"control_bottom_app_bar_archive": "Archivia",
|
"control_bottom_app_bar_archive": "Archivia",
|
||||||
"control_bottom_app_bar_create_new_album": "Crea nuovo album",
|
"control_bottom_app_bar_create_new_album": "Crea nuovo album",
|
||||||
"control_bottom_app_bar_delete": "Elimina",
|
"control_bottom_app_bar_delete": "Elimina",
|
||||||
"control_bottom_app_bar_delete_from_immich": "Elimina da Immich",
|
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||||
"control_bottom_app_bar_delete_from_local": "Elimina dal dispositivo",
|
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||||
"control_bottom_app_bar_edit_location": "Modifica posizione",
|
"control_bottom_app_bar_edit_location": "Edit Location",
|
||||||
"control_bottom_app_bar_edit_time": "Modifica data e ora",
|
"control_bottom_app_bar_edit_time": "Edit Date & Time",
|
||||||
"control_bottom_app_bar_favorite": "Preferiti",
|
"control_bottom_app_bar_favorite": "Preferiti",
|
||||||
"control_bottom_app_bar_share": "Condividi",
|
"control_bottom_app_bar_share": "Condividi",
|
||||||
"control_bottom_app_bar_share_to": "Share To",
|
"control_bottom_app_bar_share_to": "Share To",
|
||||||
"control_bottom_app_bar_stack": "Stack",
|
"control_bottom_app_bar_stack": "Stack",
|
||||||
"control_bottom_app_bar_trash_from_immich": "Sposta nel cestino",
|
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
|
||||||
"control_bottom_app_bar_unarchive": "Rimuovi dagli archivi",
|
"control_bottom_app_bar_unarchive": "Rimuovi dagli archivi",
|
||||||
"control_bottom_app_bar_unfavorite": "Unfavorite",
|
"control_bottom_app_bar_unfavorite": "Unfavorite",
|
||||||
"control_bottom_app_bar_upload": "Upload",
|
"control_bottom_app_bar_upload": "Upload",
|
||||||
@@ -165,27 +165,26 @@
|
|||||||
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||||
"date_format": "E, d LLL, y • hh:mm",
|
"date_format": "E, d LLL, y • hh:mm",
|
||||||
"delete_dialog_alert": "Questi oggetti saranno cancellati definitivamente da Immich e dal tuo device",
|
"delete_dialog_alert": "Questi oggetti saranno cancellati definitivamente da Immich e dal tuo device",
|
||||||
"delete_dialog_alert_local": "Questi elementi verranno eliminati definitivamente dal dispositivo, ma saranno ancora disponibili sul server Immich",
|
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
|
||||||
"delete_dialog_alert_local_non_backed_up": "Alcuni degli elementi non sono stati caricati su Immich e saranno rimossi definitivamente dal tuo dispositivo",
|
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
|
||||||
"delete_dialog_alert_remote": "Questi elementi verranno eliminati permanentemente dal server Immich",
|
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
|
||||||
"delete_dialog_cancel": "Annulla",
|
"delete_dialog_cancel": "Annulla",
|
||||||
"delete_dialog_ok": "Elimina",
|
"delete_dialog_ok": "Elimina",
|
||||||
"delete_dialog_ok_force": "Elimina comunque",
|
"delete_dialog_ok_force": "Delete Anyway",
|
||||||
"delete_dialog_title": "Cancella definitivamente",
|
"delete_dialog_title": "Cancella definitivamente",
|
||||||
"delete_local_dialog_ok_backed_up_only": "Elimina solo quelli con backup",
|
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||||
"delete_local_dialog_ok_force": "Elimina comunque",
|
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||||
"delete_shared_link_dialog_content": "Sei sicuro di voler eliminare questo link condiviso?",
|
"delete_shared_link_dialog_content": "Sei sicuro di voler eliminare questo link condiviso?",
|
||||||
"delete_shared_link_dialog_title": "Elimina link condiviso",
|
"delete_shared_link_dialog_title": "Elimina link condiviso",
|
||||||
"description_input_hint_text": "Aggiungi descrizione...",
|
"description_input_hint_text": "Aggiungi descrizione...",
|
||||||
"description_input_submit_error": "Errore modificare descrizione, controlli I log per maggiori dettagli",
|
"description_input_submit_error": "Errore modificare descrizione, controlli I log per maggiori dettagli",
|
||||||
"edit_date_time_dialog_date_time": "Data e ora",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Fuso orario",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_location_dialog_title": "Posizione",
|
"edit_location_dialog_title": "Location",
|
||||||
"exif_bottom_sheet_description": "Aggiungi una descrizione...",
|
"exif_bottom_sheet_description": "Aggiungi una descrizione...",
|
||||||
"exif_bottom_sheet_details": "DETTAGLI",
|
"exif_bottom_sheet_details": "DETTAGLI",
|
||||||
"exif_bottom_sheet_location": "POSIZIONE",
|
"exif_bottom_sheet_location": "POSIZIONE",
|
||||||
"exif_bottom_sheet_location_add": "Aggiungi una posizione",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||||
"experimental_settings_new_asset_list_title": "Attiva griglia di foto sperimentale",
|
"experimental_settings_new_asset_list_title": "Attiva griglia di foto sperimentale",
|
||||||
"experimental_settings_subtitle": "Usalo a tuo rischio!",
|
"experimental_settings_subtitle": "Usalo a tuo rischio!",
|
||||||
@@ -200,7 +199,7 @@
|
|||||||
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
|
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
|
||||||
"home_page_building_timeline": "Costruendo il Timeline",
|
"home_page_building_timeline": "Costruendo il Timeline",
|
||||||
"home_page_delete_err_partner": "Can not delete partner assets, skipping",
|
"home_page_delete_err_partner": "Can not delete partner assets, skipping",
|
||||||
"home_page_delete_remote_err_local": "Immagini sul disco locale presenti pure nella selezione degli elementi remoti, skippando",
|
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
|
||||||
"home_page_favorite_err_local": "Non puoi aggiungere tra i preferiti le foto ancora non caricate",
|
"home_page_favorite_err_local": "Non puoi aggiungere tra i preferiti le foto ancora non caricate",
|
||||||
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
|
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
|
||||||
"home_page_first_time_notice": "Se è la prima volta che usi l'app, assicurati di scegliere gli album per avere il Timeline con immagini e video",
|
"home_page_first_time_notice": "Se è la prima volta che usi l'app, assicurati di scegliere gli album per avere il Timeline con immagini e video",
|
||||||
@@ -215,22 +214,22 @@
|
|||||||
"library_page_favorites": "Preferiti",
|
"library_page_favorites": "Preferiti",
|
||||||
"library_page_new_album": "Nuovo Album",
|
"library_page_new_album": "Nuovo Album",
|
||||||
"library_page_sharing": "Condividendo",
|
"library_page_sharing": "Condividendo",
|
||||||
"library_page_sort_asset_count": "Numero di elementi",
|
"library_page_sort_asset_count": "Number of assets",
|
||||||
"library_page_sort_created": "Creato il più recente",
|
"library_page_sort_created": "Creato il più recente",
|
||||||
"library_page_sort_last_modified": "Ultima modifica",
|
"library_page_sort_last_modified": "Ultima modifica",
|
||||||
"library_page_sort_most_oldest_photo": "Foto più vecchia",
|
"library_page_sort_most_oldest_photo": "Oldest photo",
|
||||||
"library_page_sort_most_recent_photo": "Più recente",
|
"library_page_sort_most_recent_photo": "Più recente",
|
||||||
"library_page_sort_title": "Titolo album",
|
"library_page_sort_title": "Titolo album",
|
||||||
"location_picker_choose_on_map": "Scegli una mappa",
|
"location_picker_choose_on_map": "Choose on map",
|
||||||
"location_picker_latitude": "Latitudine",
|
"location_picker_latitude": "Latitude",
|
||||||
"location_picker_latitude_error": "Inserisci una latitudine valida",
|
"location_picker_latitude_error": "Enter a valid latitude",
|
||||||
"location_picker_latitude_hint": "Inserisci la tua latitudine qui",
|
"location_picker_latitude_hint": "Enter your latitude here",
|
||||||
"location_picker_longitude": "Longitudine",
|
"location_picker_longitude": "Longitude",
|
||||||
"location_picker_longitude_error": "Inserisci una longitudine valida",
|
"location_picker_longitude_error": "Enter a valid longitude",
|
||||||
"location_picker_longitude_hint": "Inserisci la longitudine qui",
|
"location_picker_longitude_hint": "Enter your longitude here",
|
||||||
"login_disabled": "L'accesso è stato disattivato",
|
"login_disabled": "L'accesso è stato disattivato",
|
||||||
"login_form_api_exception": "API error, per favore ricontrolli URL del server e riprovi",
|
"login_form_api_exception": "API error, per favore ricontrolli URL del server e riprovi",
|
||||||
"login_form_back_button_text": "Indietro",
|
"login_form_back_button_text": "Back",
|
||||||
"login_form_button_text": "Login",
|
"login_form_button_text": "Login",
|
||||||
"login_form_email_hint": "tuaemail@email.com",
|
"login_form_email_hint": "tuaemail@email.com",
|
||||||
"login_form_endpoint_hint": "http://ip-del-tuo-server:port/api",
|
"login_form_endpoint_hint": "http://ip-del-tuo-server:port/api",
|
||||||
@@ -253,35 +252,35 @@
|
|||||||
"login_form_server_error": "Non è possibile connettersi al server",
|
"login_form_server_error": "Non è possibile connettersi al server",
|
||||||
"login_password_changed_error": "C'è stato un errore durante l'aggiornamento della password",
|
"login_password_changed_error": "C'è stato un errore durante l'aggiornamento della password",
|
||||||
"login_password_changed_success": "Password aggiornata con successo",
|
"login_password_changed_success": "Password aggiornata con successo",
|
||||||
"map_assets_in_bound": "{} foto",
|
"map_assets_in_bound": "{} photo",
|
||||||
"map_assets_in_bounds": "{} foto",
|
"map_assets_in_bounds": "{} photos",
|
||||||
"map_cannot_get_user_location": "Cannot get user's location",
|
"map_cannot_get_user_location": "Cannot get user's location",
|
||||||
"map_location_dialog_cancel": "Annulla",
|
"map_location_dialog_cancel": "Annulla",
|
||||||
"map_location_dialog_yes": "Si",
|
"map_location_dialog_yes": "Si",
|
||||||
"map_location_picker_page_use_location": "Usa questa posizione",
|
"map_location_picker_page_use_location": "Use this location",
|
||||||
"map_location_service_disabled_content": "I servizi di geolocalizzazione devono essere attivati per visualizzare gli elementi per la tua posizione attuale. Vuoi attivarli adesso?",
|
"map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?",
|
||||||
"map_location_service_disabled_title": "Location Service disabled",
|
"map_location_service_disabled_title": "Location Service disabled",
|
||||||
"map_no_assets_in_bounds": "Nessuna foto in questa zona",
|
"map_no_assets_in_bounds": "Nessuna foto in questa zona",
|
||||||
"map_no_location_permission_content": "L'accesso alla posizione è necessario per visualizzare gli elementi per la tua posizione attuale. Vuoi consentirlo adesso?",
|
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
||||||
"map_no_location_permission_title": "Location Permission denied",
|
"map_no_location_permission_title": "Location Permission denied",
|
||||||
"map_settings_dark_mode": "Modalità scura",
|
"map_settings_dark_mode": "Modalità scura",
|
||||||
"map_settings_date_range_option_all": "All",
|
"map_settings_date_range_option_all": "All",
|
||||||
"map_settings_date_range_option_day": "Ultime 24 ore",
|
"map_settings_date_range_option_day": "Past 24 hours",
|
||||||
"map_settings_date_range_option_days": "Ultimi {} giorni",
|
"map_settings_date_range_option_days": "Past {} days",
|
||||||
"map_settings_date_range_option_year": "Ultimo anno",
|
"map_settings_date_range_option_year": "Past year",
|
||||||
"map_settings_date_range_option_years": "Ultimi {} anni",
|
"map_settings_date_range_option_years": "Past {} years",
|
||||||
"map_settings_dialog_cancel": "Cancel",
|
"map_settings_dialog_cancel": "Cancel",
|
||||||
"map_settings_dialog_save": "Salva",
|
"map_settings_dialog_save": "Salva",
|
||||||
"map_settings_dialog_title": "Map Settings",
|
"map_settings_dialog_title": "Map Settings",
|
||||||
"map_settings_include_show_archived": "Include Archived",
|
"map_settings_include_show_archived": "Include Archived",
|
||||||
"map_settings_only_relative_range": "Date range",
|
"map_settings_only_relative_range": "Date range",
|
||||||
"map_settings_only_show_favorites": "Show Favorite Only",
|
"map_settings_only_show_favorites": "Show Favorite Only",
|
||||||
"map_settings_theme_settings": "Tema della mappa",
|
"map_settings_theme_settings": "Map Theme",
|
||||||
"map_zoom_to_see_photos": "Zoom out to see photos",
|
"map_zoom_to_see_photos": "Zoom out to see photos",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"motion_photos_page_title": "Motion Foto",
|
"motion_photos_page_title": "Motion Foto",
|
||||||
"multiselect_grid_edit_date_time_err_read_only": "Non posso modificare la data degli elementi in sola lettura, ignorato",
|
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||||
"multiselect_grid_edit_gps_err_read_only": "Non posso modificare la posizione degli elementi in sola lettura, ignorato",
|
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
||||||
"notification_permission_dialog_cancel": "Annulla",
|
"notification_permission_dialog_cancel": "Annulla",
|
||||||
"notification_permission_dialog_content": "Per attivare le notifiche, vai alle Impostazioni e seleziona concedi",
|
"notification_permission_dialog_content": "Per attivare le notifiche, vai alle Impostazioni e seleziona concedi",
|
||||||
"notification_permission_dialog_settings": "Impostazioni",
|
"notification_permission_dialog_settings": "Impostazioni",
|
||||||
@@ -308,18 +307,18 @@
|
|||||||
"permission_onboarding_permission_limited": "Permessi limitati. Perché Immich possa controllare e fare i backup di tutte le foto, concedere i permessi all'intera galleria dalle impostazioni ",
|
"permission_onboarding_permission_limited": "Permessi limitati. Perché Immich possa controllare e fare i backup di tutte le foto, concedere i permessi all'intera galleria dalle impostazioni ",
|
||||||
"permission_onboarding_request": "Immich richiede i permessi per vedere le tue foto e video",
|
"permission_onboarding_request": "Immich richiede i permessi per vedere le tue foto e video",
|
||||||
"profile_drawer_app_logs": "Logs",
|
"profile_drawer_app_logs": "Logs",
|
||||||
"profile_drawer_client_out_of_date_major": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione principale.",
|
"profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
|
||||||
"profile_drawer_client_out_of_date_minor": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione minore.",
|
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
|
||||||
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
|
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
|
||||||
"profile_drawer_documentation": "Documentazione",
|
"profile_drawer_documentation": "Documentazione",
|
||||||
"profile_drawer_github": "GitHub",
|
"profile_drawer_github": "GitHub",
|
||||||
"profile_drawer_server_out_of_date_major": "Il server non è aggiornato. Per favore aggiorna all'ultima versione principale.",
|
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
|
||||||
"profile_drawer_server_out_of_date_minor": "Il server non è aggiornato. Per favore aggiorna all'ultima versione minore.",
|
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
|
||||||
"profile_drawer_settings": "Impostazioni ",
|
"profile_drawer_settings": "Impostazioni ",
|
||||||
"profile_drawer_sign_out": "Logout",
|
"profile_drawer_sign_out": "Logout",
|
||||||
"profile_drawer_trash": "Trash",
|
"profile_drawer_trash": "Trash",
|
||||||
"recently_added_page_title": "Aggiunti di recente",
|
"recently_added_page_title": "Aggiunti di recente",
|
||||||
"scaffold_body_error_occurred": "Si è verificato un errore.",
|
"scaffold_body_error_occurred": "Error occurred",
|
||||||
"search_bar_hint": "Cerca le tue foto",
|
"search_bar_hint": "Cerca le tue foto",
|
||||||
"search_page_categories": "Categoria",
|
"search_page_categories": "Categoria",
|
||||||
"search_page_favorites": "Preferiti",
|
"search_page_favorites": "Preferiti",
|
||||||
@@ -327,13 +326,13 @@
|
|||||||
"search_page_no_objects": "Nessuna informazione relativa all'oggetto disponibile",
|
"search_page_no_objects": "Nessuna informazione relativa all'oggetto disponibile",
|
||||||
"search_page_no_places": "Nessun informazione sul luogo disponibile",
|
"search_page_no_places": "Nessun informazione sul luogo disponibile",
|
||||||
"search_page_people": "Persone",
|
"search_page_people": "Persone",
|
||||||
"search_page_person_add_name_dialog_cancel": "Annulla",
|
"search_page_person_add_name_dialog_cancel": "Cancel",
|
||||||
"search_page_person_add_name_dialog_hint": "Nome",
|
"search_page_person_add_name_dialog_hint": "Name",
|
||||||
"search_page_person_add_name_dialog_save": "Salva",
|
"search_page_person_add_name_dialog_save": "Save",
|
||||||
"search_page_person_add_name_dialog_title": "Aggiungi un nome",
|
"search_page_person_add_name_dialog_title": "Add a name",
|
||||||
"search_page_person_add_name_subtitle": "Find them fast by name with search",
|
"search_page_person_add_name_subtitle": "Find them fast by name with search",
|
||||||
"search_page_person_add_name_title": "Aggiungi un nome",
|
"search_page_person_add_name_title": "Add a name",
|
||||||
"search_page_person_edit_name": "Modifica nome",
|
"search_page_person_edit_name": "Edit name",
|
||||||
"search_page_places": "Luoghi",
|
"search_page_places": "Luoghi",
|
||||||
"search_page_recently_added": "Aggiunte di recente",
|
"search_page_recently_added": "Aggiunte di recente",
|
||||||
"search_page_screenshots": "Screenshot",
|
"search_page_screenshots": "Screenshot",
|
||||||
@@ -342,7 +341,7 @@
|
|||||||
"search_page_videos": "Video",
|
"search_page_videos": "Video",
|
||||||
"search_page_view_all_button": "Guarda tutto",
|
"search_page_view_all_button": "Guarda tutto",
|
||||||
"search_page_your_activity": "Tua attività ",
|
"search_page_your_activity": "Tua attività ",
|
||||||
"search_page_your_map": "La tua mappa",
|
"search_page_your_map": "Your Map",
|
||||||
"search_result_page_new_search_hint": "Nuova ricerca ",
|
"search_result_page_new_search_hint": "Nuova ricerca ",
|
||||||
"search_suggestion_list_smart_search_hint_1": "\nRicerca Smart è attiva di default, per usare la ricerca con i metadata usare la seguente sintassi",
|
"search_suggestion_list_smart_search_hint_1": "\nRicerca Smart è attiva di default, per usare la ricerca con i metadata usare la seguente sintassi",
|
||||||
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
|
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
|
||||||
@@ -382,17 +381,17 @@
|
|||||||
"shared_album_activity_remove_title": "Elimina attività",
|
"shared_album_activity_remove_title": "Elimina attività",
|
||||||
"shared_album_activity_setting_subtitle": "Let others respond",
|
"shared_album_activity_setting_subtitle": "Let others respond",
|
||||||
"shared_album_activity_setting_title": "Commenti e Mi piace",
|
"shared_album_activity_setting_title": "Commenti e Mi piace",
|
||||||
"shared_album_section_people_action_error": "Errore durante la rimozione/uscita dell'album",
|
"shared_album_section_people_action_error": "Error leaving/removing from album",
|
||||||
"shared_album_section_people_action_leave": "Rimuovi utente dall'album",
|
"shared_album_section_people_action_leave": "Remove user from album",
|
||||||
"shared_album_section_people_action_remove_user": "Rimuovi utente dall'album",
|
"shared_album_section_people_action_remove_user": "Remove user from album",
|
||||||
"shared_album_section_people_owner_label": "Proprietario",
|
"shared_album_section_people_owner_label": "Owner",
|
||||||
"shared_album_section_people_title": "PERSONE",
|
"shared_album_section_people_title": "PEOPLE",
|
||||||
"share_dialog_preparing": "Preparo…",
|
"share_dialog_preparing": "Preparo…",
|
||||||
"shared_link_app_bar_title": "Link condivisi",
|
"shared_link_app_bar_title": "Link condivisi",
|
||||||
"shared_link_clipboard_copied_massage": "Copiato negli appunti",
|
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
||||||
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
|
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
|
||||||
"shared_link_create_app_bar_title": "Crea link di condivisione",
|
"shared_link_create_app_bar_title": "Crea link di condivisione",
|
||||||
"shared_link_create_error": "Si è verificato un errore durante la creazione del link condiviso",
|
"shared_link_create_error": "Error while creating shared link",
|
||||||
"shared_link_create_info": "Consenti a chiunque abbia il link di vedere le foto selezionate",
|
"shared_link_create_info": "Consenti a chiunque abbia il link di vedere le foto selezionate",
|
||||||
"shared_link_create_submit_button": "Crea link di condivisione",
|
"shared_link_create_submit_button": "Crea link di condivisione",
|
||||||
"shared_link_edit_allow_download": "Consenti ad utenti pubblici di scaricare i contenuti",
|
"shared_link_edit_allow_download": "Consenti ad utenti pubblici di scaricare i contenuti",
|
||||||
@@ -402,32 +401,32 @@
|
|||||||
"shared_link_edit_description": "Descrizione",
|
"shared_link_edit_description": "Descrizione",
|
||||||
"shared_link_edit_description_hint": "Inserisci la descrizione della condivisione",
|
"shared_link_edit_description_hint": "Inserisci la descrizione della condivisione",
|
||||||
"shared_link_edit_expire_after": "Scade dopo",
|
"shared_link_edit_expire_after": "Scade dopo",
|
||||||
"shared_link_edit_expire_after_option_day": "1 giorno",
|
"shared_link_edit_expire_after_option_day": "1 day",
|
||||||
"shared_link_edit_expire_after_option_days": "{} giorni",
|
"shared_link_edit_expire_after_option_days": "{} days",
|
||||||
"shared_link_edit_expire_after_option_hour": "1 ora",
|
"shared_link_edit_expire_after_option_hour": "1 hour",
|
||||||
"shared_link_edit_expire_after_option_hours": "{} ore",
|
"shared_link_edit_expire_after_option_hours": "{} hours",
|
||||||
"shared_link_edit_expire_after_option_minute": "1 minuto",
|
"shared_link_edit_expire_after_option_minute": "1 minute",
|
||||||
"shared_link_edit_expire_after_option_minutes": "{} minuti",
|
"shared_link_edit_expire_after_option_minutes": "{} minutes",
|
||||||
"shared_link_edit_expire_after_option_never": "Mai",
|
"shared_link_edit_expire_after_option_never": "Never",
|
||||||
"shared_link_edit_password": "Password",
|
"shared_link_edit_password": "Password",
|
||||||
"shared_link_edit_password_hint": "Inserire la password di condivisione",
|
"shared_link_edit_password_hint": "Inserire la password di condivisione",
|
||||||
"shared_link_edit_show_meta": "Visualizza metadati",
|
"shared_link_edit_show_meta": "Visualizza metadati",
|
||||||
"shared_link_edit_submit_button": "Aggiorna link",
|
"shared_link_edit_submit_button": "Aggiorna link",
|
||||||
"shared_link_empty": "Non hai alcun link condiviso",
|
"shared_link_empty": "Non hai alcun link condiviso",
|
||||||
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
|
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
|
||||||
"shared_link_expired": "Scaduto",
|
"shared_link_expired": "Expired",
|
||||||
"shared_link_expires_day": "Scade tra {} giorno",
|
"shared_link_expires_day": "Expires in {} day",
|
||||||
"shared_link_expires_days": "Scade tra {} giorni",
|
"shared_link_expires_days": "Expires in {} days",
|
||||||
"shared_link_expires_hour": "Scade tra {} ora",
|
"shared_link_expires_hour": "Expires in {} hour",
|
||||||
"shared_link_expires_hours": "Scade tra {} ore",
|
"shared_link_expires_hours": "Expires in {} hours",
|
||||||
"shared_link_expires_minute": "Scade tra {} minuto",
|
"shared_link_expires_minute": "Expires in {} minute",
|
||||||
"shared_link_expires_minutes": "Scade tra {} minuti",
|
"shared_link_expires_minutes": "Expires in {} minutes",
|
||||||
"shared_link_expires_never": "Scadenza ∞",
|
"shared_link_expires_never": "Expires ∞",
|
||||||
"shared_link_expires_second": "Scade tra {} secondo",
|
"shared_link_expires_second": "Expires in {} second",
|
||||||
"shared_link_expires_seconds": "Scade tra {} secondi",
|
"shared_link_expires_seconds": "Expires in {} seconds",
|
||||||
"shared_link_info_chip_download": "Scarica",
|
"shared_link_info_chip_download": "Download",
|
||||||
"shared_link_info_chip_metadata": "EXIF",
|
"shared_link_info_chip_metadata": "EXIF",
|
||||||
"shared_link_info_chip_upload": "Carica",
|
"shared_link_info_chip_upload": "Upload",
|
||||||
"shared_link_manage_links": "Gestisci link condivisi",
|
"shared_link_manage_links": "Gestisci link condivisi",
|
||||||
"share_done": "Done",
|
"share_done": "Done",
|
||||||
"share_invite": "Invita nell'album ",
|
"share_invite": "Invita nell'album ",
|
||||||
@@ -455,7 +454,7 @@
|
|||||||
"trash_page_delete": "Elimina",
|
"trash_page_delete": "Elimina",
|
||||||
"trash_page_delete_all": "Elimina tutti",
|
"trash_page_delete_all": "Elimina tutti",
|
||||||
"trash_page_empty_trash_btn": "Svuota cestino",
|
"trash_page_empty_trash_btn": "Svuota cestino",
|
||||||
"trash_page_empty_trash_dialog_content": "Vuoi eliminare gli elementi nel cestino? Questi elementi saranno eliminati definitivamente da Immich",
|
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich",
|
||||||
"trash_page_empty_trash_dialog_ok": "Ok",
|
"trash_page_empty_trash_dialog_ok": "Ok",
|
||||||
"trash_page_info": "Gli elementi cestinati saranno eliminati definitivamente dopo {} giorni",
|
"trash_page_info": "Gli elementi cestinati saranno eliminati definitivamente dopo {} giorni",
|
||||||
"trash_page_no_assets": "Nessun elemento cestinato",
|
"trash_page_no_assets": "Nessun elemento cestinato",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"action_common_cancel": "キャンセル",
|
"action_common_cancel": "Cancel",
|
||||||
"action_common_update": "更新",
|
"action_common_update": "Update",
|
||||||
"add_to_album_bottom_sheet_added": "{album}に追加",
|
"add_to_album_bottom_sheet_added": "{album}に追加",
|
||||||
"add_to_album_bottom_sheet_already_exists": "{album}に追加済み",
|
"add_to_album_bottom_sheet_already_exists": "{album}に追加済み",
|
||||||
"advanced_settings_log_level_title": "ログレベル: {}",
|
"advanced_settings_log_level_title": "Log level: {}",
|
||||||
"advanced_settings_prefer_remote_subtitle": "端末によっては端末上に存在するサムネイルのロードに非常に時間がかかります。このオプションをに有効にする事によってサーバーから直接画像をロードすることが可能です",
|
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
|
||||||
"advanced_settings_prefer_remote_title": "リモートを優先する",
|
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
||||||
"advanced_settings_self_signed_ssl_subtitle": "SSLのチェックをスキップする。Self-signedな署名で必要です",
|
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
||||||
"advanced_settings_self_signed_ssl_title": "Self-signed署名を許可する",
|
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
||||||
"advanced_settings_tile_subtitle": "追加ユーザー設定",
|
"advanced_settings_tile_subtitle": "追加ユーザー設定",
|
||||||
"advanced_settings_tile_title": "詳細設定",
|
"advanced_settings_tile_title": "詳細設定",
|
||||||
"advanced_settings_troubleshooting_subtitle": "トラブルシューティング用の詳細設定をオンにする",
|
"advanced_settings_troubleshooting_subtitle": "トラブルシューティング用の詳細設定をオンにする",
|
||||||
@@ -26,17 +26,17 @@
|
|||||||
"album_viewer_appbar_share_err_title": "タイトル変更の失敗",
|
"album_viewer_appbar_share_err_title": "タイトル変更の失敗",
|
||||||
"album_viewer_appbar_share_leave": "アルバムから脱退",
|
"album_viewer_appbar_share_leave": "アルバムから脱退",
|
||||||
"album_viewer_appbar_share_remove": "アルバムから削除",
|
"album_viewer_appbar_share_remove": "アルバムから削除",
|
||||||
"album_viewer_appbar_share_to": "次の方々と共有します",
|
"album_viewer_appbar_share_to": "Share To",
|
||||||
"album_viewer_page_share_add_users": "ユーザーを追加",
|
"album_viewer_page_share_add_users": "ユーザーを追加",
|
||||||
"all_people_page_title": "ピープル",
|
"all_people_page_title": "People",
|
||||||
"all_videos_page_title": "ビデオ",
|
"all_videos_page_title": "ビデオ",
|
||||||
"app_bar_signout_dialog_content": " サインアウトしますか?",
|
"app_bar_signout_dialog_content": " サインアウトしますか?",
|
||||||
"app_bar_signout_dialog_ok": "はい",
|
"app_bar_signout_dialog_ok": "はい",
|
||||||
"app_bar_signout_dialog_title": " サインアウト",
|
"app_bar_signout_dialog_title": " サインアウト",
|
||||||
"archive_page_no_archived_assets": "アーカイブ済みの写真またはビデオがありません",
|
"archive_page_no_archived_assets": "アーカイブ済みの写真またはビデオがありません",
|
||||||
"archive_page_title": "アーカイブ({})",
|
"archive_page_title": "アーカイブ({})",
|
||||||
"asset_action_delete_err_read_only": "読み取り専用の項目は削除できません。スキップします",
|
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
|
||||||
"asset_action_share_err_offline": "オフラインの項目をゲットできません。スキップします",
|
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||||
"asset_list_layout_settings_dynamic_layout_title": "ダイナミックレイアウト",
|
"asset_list_layout_settings_dynamic_layout_title": "ダイナミックレイアウト",
|
||||||
"asset_list_layout_settings_group_automatically": "自動",
|
"asset_list_layout_settings_group_automatically": "自動",
|
||||||
"asset_list_layout_settings_group_by": "写真のグループ分け",
|
"asset_list_layout_settings_group_by": "写真のグループ分け",
|
||||||
@@ -103,17 +103,17 @@
|
|||||||
"backup_controller_page_uploading_file_info": "アップロード中のファイル",
|
"backup_controller_page_uploading_file_info": "アップロード中のファイル",
|
||||||
"backup_err_only_album": "最低1つのアルバムを選択してください",
|
"backup_err_only_album": "最低1つのアルバムを選択してください",
|
||||||
"backup_info_card_assets": "写真と動画",
|
"backup_info_card_assets": "写真と動画",
|
||||||
"backup_manual_cancelled": "キャンセルされました",
|
"backup_manual_cancelled": "Cancelled",
|
||||||
"backup_manual_failed": "失敗",
|
"backup_manual_failed": "Failed",
|
||||||
"backup_manual_in_progress": "アップロードが進行中です。後でもう一度試してください",
|
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
|
||||||
"backup_manual_success": "成功",
|
"backup_manual_success": "Success",
|
||||||
"backup_manual_title": "アップロード状況",
|
"backup_manual_title": "Upload status",
|
||||||
"cache_settings_album_thumbnails": "ライブラリのサムネイル ({}枚)",
|
"cache_settings_album_thumbnails": "ライブラリのサムネイル ({}枚)",
|
||||||
"cache_settings_clear_cache_button": "キャッシュをクリア",
|
"cache_settings_clear_cache_button": "キャッシュをクリア",
|
||||||
"cache_settings_clear_cache_button_title": "キャッシュを削除(キャッシュ再生成までアプリのパフォーマンスが著しく低下)",
|
"cache_settings_clear_cache_button_title": "キャッシュを削除(キャッシュ再生成までアプリのパフォーマンスが著しく低下)",
|
||||||
"cache_settings_duplicated_assets_clear_button": "クリア",
|
"cache_settings_duplicated_assets_clear_button": "CLEAR",
|
||||||
"cache_settings_duplicated_assets_subtitle": "アプリがブラックリストに追加している項目",
|
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
|
||||||
"cache_settings_duplicated_assets_title": "{}項目が重複",
|
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
|
||||||
"cache_settings_image_cache_size": "キャッシュのサイズ ({}枚) ",
|
"cache_settings_image_cache_size": "キャッシュのサイズ ({}枚) ",
|
||||||
"cache_settings_statistics_album": "ライブラリのサムネイル",
|
"cache_settings_statistics_album": "ライブラリのサムネイル",
|
||||||
"cache_settings_statistics_assets": "{} 枚 ({}枚中)",
|
"cache_settings_statistics_assets": "{} 枚 ({}枚中)",
|
||||||
@@ -123,8 +123,8 @@
|
|||||||
"cache_settings_statistics_title": "キャッシュ",
|
"cache_settings_statistics_title": "キャッシュ",
|
||||||
"cache_settings_subtitle": "キャッシュの動作を変更する",
|
"cache_settings_subtitle": "キャッシュの動作を変更する",
|
||||||
"cache_settings_thumbnail_size": "サムネイルのキャッシュのサイズ ({}枚)",
|
"cache_settings_thumbnail_size": "サムネイルのキャッシュのサイズ ({}枚)",
|
||||||
"cache_settings_tile_subtitle": "ローカルストレージの挙動を確認する",
|
"cache_settings_tile_subtitle": "Control the local storage behaviour",
|
||||||
"cache_settings_tile_title": "ローカルストレージ",
|
"cache_settings_tile_title": "Local Storage",
|
||||||
"cache_settings_title": "キャッシュの設定",
|
"cache_settings_title": "キャッシュの設定",
|
||||||
"change_password_form_confirm_password": "確定",
|
"change_password_form_confirm_password": "確定",
|
||||||
"change_password_form_description": "{name}さん こんにちは\n\nサーバーにアクセスするのが初めてか、パスワードリセットのリクエストがされました。新しいパスワードを入力してください",
|
"change_password_form_description": "{name}さん こんにちは\n\nサーバーにアクセスするのが初めてか、パスワードリセットのリクエストがされました。新しいパスワードを入力してください",
|
||||||
@@ -142,18 +142,18 @@
|
|||||||
"control_bottom_app_bar_archive": "アーカイブ",
|
"control_bottom_app_bar_archive": "アーカイブ",
|
||||||
"control_bottom_app_bar_create_new_album": "アルバムを作成",
|
"control_bottom_app_bar_create_new_album": "アルバムを作成",
|
||||||
"control_bottom_app_bar_delete": "削除",
|
"control_bottom_app_bar_delete": "削除",
|
||||||
"control_bottom_app_bar_delete_from_immich": "Immichから削除",
|
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||||
"control_bottom_app_bar_delete_from_local": "端末から削除",
|
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||||
"control_bottom_app_bar_edit_location": "位置情報を編集",
|
"control_bottom_app_bar_edit_location": "Edit Location",
|
||||||
"control_bottom_app_bar_edit_time": "日時を変更",
|
"control_bottom_app_bar_edit_time": "Edit Date & Time",
|
||||||
"control_bottom_app_bar_favorite": "お気に入り",
|
"control_bottom_app_bar_favorite": "お気に入り",
|
||||||
"control_bottom_app_bar_share": "共有",
|
"control_bottom_app_bar_share": "共有",
|
||||||
"control_bottom_app_bar_share_to": "次のユーザーに共有: ",
|
"control_bottom_app_bar_share_to": "Share To",
|
||||||
"control_bottom_app_bar_stack": "スタック",
|
"control_bottom_app_bar_stack": "Stack",
|
||||||
"control_bottom_app_bar_trash_from_immich": "ゴミ箱に捨てる",
|
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
|
||||||
"control_bottom_app_bar_unarchive": "アーカイブを解除",
|
"control_bottom_app_bar_unarchive": "アーカイブを解除",
|
||||||
"control_bottom_app_bar_unfavorite": "お気に入りから外す",
|
"control_bottom_app_bar_unfavorite": "Unfavorite",
|
||||||
"control_bottom_app_bar_upload": "アップロード",
|
"control_bottom_app_bar_upload": "Upload",
|
||||||
"create_album_page_untitled": "タイトルなし",
|
"create_album_page_untitled": "タイトルなし",
|
||||||
"create_shared_album_page_create": "作成",
|
"create_shared_album_page_create": "作成",
|
||||||
"create_shared_album_page_share": "共有",
|
"create_shared_album_page_share": "共有",
|
||||||
@@ -165,27 +165,26 @@
|
|||||||
"daily_title_text_date_year": "yyyy年 MM月 DD日, EE",
|
"daily_title_text_date_year": "yyyy年 MM月 DD日, EE",
|
||||||
"date_format": "MM月 DD日, EE • hh時mm分",
|
"date_format": "MM月 DD日, EE • hh時mm分",
|
||||||
"delete_dialog_alert": "サーバーとデバイスの両方から永久的に削除されます!",
|
"delete_dialog_alert": "サーバーとデバイスの両方から永久的に削除されます!",
|
||||||
"delete_dialog_alert_local": "選択された項目は端末から削除されますがImmichには残ります",
|
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
|
||||||
"delete_dialog_alert_local_non_backed_up": "Immichにバックアップされていない項目があります。それらの項目はデバイスからも永久に削除されます",
|
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
|
||||||
"delete_dialog_alert_remote": "選択された項目はImmichから永久に削除されます",
|
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
|
||||||
"delete_dialog_cancel": "キャンセル",
|
"delete_dialog_cancel": "キャンセル",
|
||||||
"delete_dialog_ok": "削除",
|
"delete_dialog_ok": "削除",
|
||||||
"delete_dialog_ok_force": "削除します",
|
"delete_dialog_ok_force": "Delete Anyway",
|
||||||
"delete_dialog_title": "永久的に削除",
|
"delete_dialog_title": "永久的に削除",
|
||||||
"delete_local_dialog_ok_backed_up_only": "バックアップ済みのみを削除",
|
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||||
"delete_local_dialog_ok_force": "削除します",
|
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||||
"delete_shared_link_dialog_content": "本当にこの共有リンクを消しますか?",
|
"delete_shared_link_dialog_content": "本当にこの共有リンクを消しますか?",
|
||||||
"delete_shared_link_dialog_title": "共有リンクを消す",
|
"delete_shared_link_dialog_title": "共有リンクを消す",
|
||||||
"description_input_hint_text": "説明を追加",
|
"description_input_hint_text": "説明を追加",
|
||||||
"description_input_submit_error": "説明の編集に失敗、詳細の確認はログで行ってください",
|
"description_input_submit_error": "説明の編集に失敗、詳細の確認はログで行ってください",
|
||||||
"edit_date_time_dialog_date_time": "日付と時間",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "タイムゾーン",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_location_dialog_title": "位置情報",
|
"edit_location_dialog_title": "Location",
|
||||||
"exif_bottom_sheet_description": "説明を追加",
|
"exif_bottom_sheet_description": "説明を追加",
|
||||||
"exif_bottom_sheet_details": "詳細",
|
"exif_bottom_sheet_details": "詳細",
|
||||||
"exif_bottom_sheet_location": "撮影場所",
|
"exif_bottom_sheet_location": "撮影場所",
|
||||||
"exif_bottom_sheet_location_add": "位置情報を追加",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "製作途中(WIP)",
|
"experimental_settings_new_asset_list_subtitle": "製作途中(WIP)",
|
||||||
"experimental_settings_new_asset_list_title": "試験的なグリッドを有効化",
|
"experimental_settings_new_asset_list_title": "試験的なグリッドを有効化",
|
||||||
"experimental_settings_subtitle": "試験的機能につき自己責任で!",
|
"experimental_settings_subtitle": "試験的機能につき自己責任で!",
|
||||||
@@ -195,42 +194,42 @@
|
|||||||
"home_page_add_to_album_conflicts": "{album}に{added}枚写真を追加しました。追加済みの{failed}枚はスキップしました。",
|
"home_page_add_to_album_conflicts": "{album}に{added}枚写真を追加しました。追加済みの{failed}枚はスキップしました。",
|
||||||
"home_page_add_to_album_err_local": "まだアップロードされてない項目はアルバムに登録できません",
|
"home_page_add_to_album_err_local": "まだアップロードされてない項目はアルバムに登録できません",
|
||||||
"home_page_add_to_album_success": "{album}に{added}枚写真を追加しました",
|
"home_page_add_to_album_success": "{album}に{added}枚写真を追加しました",
|
||||||
"home_page_album_err_partner": "まだパートナーの写真はアルバムに追加できません。スキップします(アップデート待ってね)",
|
"home_page_album_err_partner": "Can not add partner assets to an album yet, skipping",
|
||||||
"home_page_archive_err_local": "まだアップロードされてない項目はアーカイブできません",
|
"home_page_archive_err_local": "まだアップロードされてない項目はアーカイブできません",
|
||||||
"home_page_archive_err_partner": "パートナーの写真はアーカイブできません。スキップします",
|
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
|
||||||
"home_page_building_timeline": "タイムライン構築中",
|
"home_page_building_timeline": "タイムライン構築中",
|
||||||
"home_page_delete_err_partner": "パートナーの写真は削除できません。スキップします",
|
"home_page_delete_err_partner": "Can not delete partner assets, skipping",
|
||||||
"home_page_delete_remote_err_local": "リモート削除の選択にローカルなアイテムが含まれています。スキップします",
|
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
|
||||||
"home_page_favorite_err_local": "まだアップロードされてない項目はお気に入り登録できません",
|
"home_page_favorite_err_local": "まだアップロードされてない項目はお気に入り登録できません",
|
||||||
"home_page_favorite_err_partner": "まだパートナーの写真をお気に入り登録できません。スキップします(アップデート待ってね)",
|
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
|
||||||
"home_page_first_time_notice": "はじめてアプリを使う場合、タイムラインに写真を表示するためにアルバムを選択してください",
|
"home_page_first_time_notice": "はじめてアプリを使う場合、タイムラインに写真を表示するためにアルバムを選択してください",
|
||||||
"home_page_share_err_local": "ローカルのみの項目をリンクで共有はできません。スキップします",
|
"home_page_share_err_local": "Can not share local assets via link, skipping",
|
||||||
"home_page_upload_err_limit": "一回でアップロードできる写真の数は30枚です。スキップします",
|
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
|
||||||
"image_viewer_page_state_provider_download_error": "ダウンロード失敗",
|
"image_viewer_page_state_provider_download_error": "ダウンロード失敗",
|
||||||
"image_viewer_page_state_provider_download_success": "ダウンロード成功",
|
"image_viewer_page_state_provider_download_success": "ダウンロード成功",
|
||||||
"image_viewer_page_state_provider_share_error": "共有エラー",
|
"image_viewer_page_state_provider_share_error": "Share Error",
|
||||||
"library_page_albums": "アルバム",
|
"library_page_albums": "アルバム",
|
||||||
"library_page_archive": "アーカイブ",
|
"library_page_archive": "アーカイブ",
|
||||||
"library_page_device_albums": "デバイス上のアルバム",
|
"library_page_device_albums": "デバイス上のアルバム",
|
||||||
"library_page_favorites": "お気に入り",
|
"library_page_favorites": "お気に入り",
|
||||||
"library_page_new_album": "新しいアルバム",
|
"library_page_new_album": "新しいアルバム",
|
||||||
"library_page_sharing": "共有中",
|
"library_page_sharing": "共有中",
|
||||||
"library_page_sort_asset_count": "項目の数",
|
"library_page_sort_asset_count": "Number of assets",
|
||||||
"library_page_sort_created": "作成日時",
|
"library_page_sort_created": "作成日時",
|
||||||
"library_page_sort_last_modified": "最終変更",
|
"library_page_sort_last_modified": "Last modified",
|
||||||
"library_page_sort_most_oldest_photo": "一番古い項目",
|
"library_page_sort_most_oldest_photo": "Oldest photo",
|
||||||
"library_page_sort_most_recent_photo": "最近の項目",
|
"library_page_sort_most_recent_photo": "Most recent photo",
|
||||||
"library_page_sort_title": "アルバム名",
|
"library_page_sort_title": "アルバム名",
|
||||||
"location_picker_choose_on_map": "マップを選択",
|
"location_picker_choose_on_map": "Choose on map",
|
||||||
"location_picker_latitude": "緯度",
|
"location_picker_latitude": "Latitude",
|
||||||
"location_picker_latitude_error": "有効な緯度を入力してください",
|
"location_picker_latitude_error": "Enter a valid latitude",
|
||||||
"location_picker_latitude_hint": "緯度をここに入力",
|
"location_picker_latitude_hint": "Enter your latitude here",
|
||||||
"location_picker_longitude": "経度",
|
"location_picker_longitude": "Longitude",
|
||||||
"location_picker_longitude_error": "有効な経度を入力してください",
|
"location_picker_longitude_error": "Enter a valid longitude",
|
||||||
"location_picker_longitude_hint": "経度をここに入力",
|
"location_picker_longitude_hint": "Enter your longitude here",
|
||||||
"login_disabled": "ログインは無効化されました",
|
"login_disabled": "Login has been disabled",
|
||||||
"login_form_api_exception": "APIエラー。URLをチェックしてもう一度試してください",
|
"login_form_api_exception": "APIエラー。URLをチェックしてもう一度試してください",
|
||||||
"login_form_back_button_text": "戻る",
|
"login_form_back_button_text": "Back",
|
||||||
"login_form_button_text": "ログイン",
|
"login_form_button_text": "ログイン",
|
||||||
"login_form_email_hint": "hoge@email.com",
|
"login_form_email_hint": "hoge@email.com",
|
||||||
"login_form_endpoint_hint": "https://example.com:port/api",
|
"login_form_endpoint_hint": "https://example.com:port/api",
|
||||||
@@ -243,7 +242,7 @@
|
|||||||
"login_form_failed_get_oauth_server_config": "OAuthログインに失敗しました。サーバーのURLを確認してください。",
|
"login_form_failed_get_oauth_server_config": "OAuthログインに失敗しました。サーバーのURLを確認してください。",
|
||||||
"login_form_failed_get_oauth_server_disable": "このサーバーではOAuthが使えません",
|
"login_form_failed_get_oauth_server_disable": "このサーバーではOAuthが使えません",
|
||||||
"login_form_failed_login": "ログインエラー。サーバーのURL・メールアドレス・パスワードを再確認してください。",
|
"login_form_failed_login": "ログインエラー。サーバーのURL・メールアドレス・パスワードを再確認してください。",
|
||||||
"login_form_handshake_exception": "Handshake Exceptionエラー。self-signed署名を設定で有効にしてください",
|
"login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.",
|
||||||
"login_form_label_email": "メールアドレス",
|
"login_form_label_email": "メールアドレス",
|
||||||
"login_form_label_password": "パスワード",
|
"login_form_label_password": "パスワード",
|
||||||
"login_form_next_button": "次",
|
"login_form_next_button": "次",
|
||||||
@@ -251,53 +250,53 @@
|
|||||||
"login_form_save_login": "ログインを保持",
|
"login_form_save_login": "ログインを保持",
|
||||||
"login_form_server_empty": "URLを入力",
|
"login_form_server_empty": "URLを入力",
|
||||||
"login_form_server_error": "サーバーに接続できません",
|
"login_form_server_error": "サーバーに接続できません",
|
||||||
"login_password_changed_error": "パスワードの変更でエラーが発生しました",
|
"login_password_changed_error": "There was an error updating your password",
|
||||||
"login_password_changed_success": "パスワードの変更に成功",
|
"login_password_changed_success": "Password updated successfully",
|
||||||
"map_assets_in_bound": "{}項目",
|
"map_assets_in_bound": "{} photo",
|
||||||
"map_assets_in_bounds": "{}項目",
|
"map_assets_in_bounds": "{} photos",
|
||||||
"map_cannot_get_user_location": "位置情報がゲットできません",
|
"map_cannot_get_user_location": "Cannot get user's location",
|
||||||
"map_location_dialog_cancel": "キャンセル",
|
"map_location_dialog_cancel": "Cancel",
|
||||||
"map_location_dialog_yes": "はい",
|
"map_location_dialog_yes": "Yes",
|
||||||
"map_location_picker_page_use_location": "この位置情報を使う",
|
"map_location_picker_page_use_location": "Use this location",
|
||||||
"map_location_service_disabled_content": "現在地の項目を表示するには位置情報がオンである必要があります。有効化しますか?",
|
"map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?",
|
||||||
"map_location_service_disabled_title": "位置情報がオフです",
|
"map_location_service_disabled_title": "Location Service disabled",
|
||||||
"map_no_assets_in_bounds": "このエリアに写真はありません",
|
"map_no_assets_in_bounds": "No photos in this area",
|
||||||
"map_no_location_permission_content": "現在地の項目を表示するには位置情報へのアクセスが必要です。許可しますか?",
|
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
||||||
"map_no_location_permission_title": "位置情報へのアクセスが拒否されました",
|
"map_no_location_permission_title": "Location Permission denied",
|
||||||
"map_settings_dark_mode": "ダークモード",
|
"map_settings_dark_mode": "Dark mode",
|
||||||
"map_settings_date_range_option_all": "全て",
|
"map_settings_date_range_option_all": "All",
|
||||||
"map_settings_date_range_option_day": "過去24時間",
|
"map_settings_date_range_option_day": "Past 24 hours",
|
||||||
"map_settings_date_range_option_days": "過去{}日間",
|
"map_settings_date_range_option_days": "Past {} days",
|
||||||
"map_settings_date_range_option_year": "過去1年",
|
"map_settings_date_range_option_year": "Past year",
|
||||||
"map_settings_date_range_option_years": "過去{}年間",
|
"map_settings_date_range_option_years": "Past {} years",
|
||||||
"map_settings_dialog_cancel": "キャンセル",
|
"map_settings_dialog_cancel": "Cancel",
|
||||||
"map_settings_dialog_save": "セーブ",
|
"map_settings_dialog_save": "Save",
|
||||||
"map_settings_dialog_title": "マップの設定",
|
"map_settings_dialog_title": "Map Settings",
|
||||||
"map_settings_include_show_archived": "アーカイブ済みを含める",
|
"map_settings_include_show_archived": "Include Archived",
|
||||||
"map_settings_only_relative_range": "Date range",
|
"map_settings_only_relative_range": "Date range",
|
||||||
"map_settings_only_show_favorites": "お気に入りのみを表示",
|
"map_settings_only_show_favorites": "Show Favorite Only",
|
||||||
"map_settings_theme_settings": "マップの見た目",
|
"map_settings_theme_settings": "Map Theme",
|
||||||
"map_zoom_to_see_photos": "写真を見るにはズームアウト",
|
"map_zoom_to_see_photos": "Zoom out to see photos",
|
||||||
"monthly_title_text_date_format": "yyyy年 MM月",
|
"monthly_title_text_date_format": "yyyy年 MM月",
|
||||||
"motion_photos_page_title": "モーションフォト",
|
"motion_photos_page_title": "モーションフォト",
|
||||||
"multiselect_grid_edit_date_time_err_read_only": "読み取り専用の項目の日付を変更できません",
|
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||||
"multiselect_grid_edit_gps_err_read_only": "読み取り専用の項目の位置情報を変更できません",
|
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
||||||
"notification_permission_dialog_cancel": "キャンセル",
|
"notification_permission_dialog_cancel": "キャンセル",
|
||||||
"notification_permission_dialog_content": "通知を許可するには設定を開いてオンにしてください",
|
"notification_permission_dialog_content": "通知を許可するには設定を開いてオンにしてください",
|
||||||
"notification_permission_dialog_settings": "設定",
|
"notification_permission_dialog_settings": "設定",
|
||||||
"notification_permission_list_tile_content": "通知の許可 をオンにしてください",
|
"notification_permission_list_tile_content": "通知の許可 をオンにしてください",
|
||||||
"notification_permission_list_tile_enable_button": "通知をオンにする",
|
"notification_permission_list_tile_enable_button": "通知をオンにする",
|
||||||
"notification_permission_list_tile_title": "通知の許可",
|
"notification_permission_list_tile_title": "通知の許可",
|
||||||
"partner_page_add_partner": "パートナーを追加",
|
"partner_page_add_partner": "Add partner",
|
||||||
"partner_page_empty_message": "まだどのパートナーとも写真を共有してません",
|
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
|
||||||
"partner_page_no_more_users": "追加できるユーザーがもういません",
|
"partner_page_no_more_users": "No more users to add",
|
||||||
"partner_page_partner_add_failed": "パートナーの追加に失敗",
|
"partner_page_partner_add_failed": "Failed to add partner",
|
||||||
"partner_page_select_partner": "パートナーを選択",
|
"partner_page_select_partner": "Select partner",
|
||||||
"partner_page_shared_to_title": "次のユーザーと共有しす: ",
|
"partner_page_shared_to_title": "Shared to",
|
||||||
"partner_page_stop_sharing_content": "{}は写真へのアクセスができなくなります",
|
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
|
||||||
"partner_page_stop_sharing_title": "写真の共有を無効化しますか?",
|
"partner_page_stop_sharing_title": "Stop sharing your photos?",
|
||||||
"partner_page_title": "パートナー",
|
"partner_page_title": "Partner",
|
||||||
"permission_onboarding_back": "戻る",
|
"permission_onboarding_back": "Back",
|
||||||
"permission_onboarding_continue_anyway": "無視して続行",
|
"permission_onboarding_continue_anyway": "無視して続行",
|
||||||
"permission_onboarding_get_started": "はじめる",
|
"permission_onboarding_get_started": "はじめる",
|
||||||
"permission_onboarding_go_to_settings": "システム設定",
|
"permission_onboarding_go_to_settings": "システム設定",
|
||||||
@@ -308,32 +307,32 @@
|
|||||||
"permission_onboarding_permission_limited": "写真へのアクセスが制限されています。Immichに写真のバックアップと管理を行わせるにはシステム設定から写真と動画のアクセス権限を変更してください。",
|
"permission_onboarding_permission_limited": "写真へのアクセスが制限されています。Immichに写真のバックアップと管理を行わせるにはシステム設定から写真と動画のアクセス権限を変更してください。",
|
||||||
"permission_onboarding_request": "Immichは写真へのアクセス許可が必要です",
|
"permission_onboarding_request": "Immichは写真へのアクセス許可が必要です",
|
||||||
"profile_drawer_app_logs": "ログ",
|
"profile_drawer_app_logs": "ログ",
|
||||||
"profile_drawer_client_out_of_date_major": "アプリが更新されてません。最新のバージョンに更新してください",
|
"profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
|
||||||
"profile_drawer_client_out_of_date_minor": "アプリが更新されてません。最新のマイナーバージョンに更新してください",
|
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
|
||||||
"profile_drawer_client_server_up_to_date": "すべて最新です",
|
"profile_drawer_client_server_up_to_date": "すべて最新です",
|
||||||
"profile_drawer_documentation": "Immcihの説明書",
|
"profile_drawer_documentation": "Documentation",
|
||||||
"profile_drawer_github": "GitHub",
|
"profile_drawer_github": "GitHub",
|
||||||
"profile_drawer_server_out_of_date_major": "サーバーが更新されてません。最新のバージョンに更新してください",
|
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
|
||||||
"profile_drawer_server_out_of_date_minor": "サーバーが更新されてません。最新のマイナーバージョンに更新してください",
|
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
|
||||||
"profile_drawer_settings": "設定",
|
"profile_drawer_settings": "設定",
|
||||||
"profile_drawer_sign_out": "サインアウト",
|
"profile_drawer_sign_out": "サインアウト",
|
||||||
"profile_drawer_trash": "ゴミ箱",
|
"profile_drawer_trash": "Trash",
|
||||||
"recently_added_page_title": "最近",
|
"recently_added_page_title": "最近",
|
||||||
"scaffold_body_error_occurred": "エラーが発生しました",
|
"scaffold_body_error_occurred": "Error occurred",
|
||||||
"search_bar_hint": "写真を検索",
|
"search_bar_hint": "写真を検索",
|
||||||
"search_page_categories": "カテゴリ",
|
"search_page_categories": "カテゴリ",
|
||||||
"search_page_favorites": "お気に入り",
|
"search_page_favorites": "お気に入り",
|
||||||
"search_page_motion_photos": "モーションフォト",
|
"search_page_motion_photos": "モーションフォト",
|
||||||
"search_page_no_objects": "被写体に関するデータがなし",
|
"search_page_no_objects": "被写体に関するデータがなし",
|
||||||
"search_page_no_places": "場所に関するデータなし",
|
"search_page_no_places": "場所に関するデータなし",
|
||||||
"search_page_people": "ピープル",
|
"search_page_people": "People",
|
||||||
"search_page_person_add_name_dialog_cancel": "キャンセル",
|
"search_page_person_add_name_dialog_cancel": "Cancel",
|
||||||
"search_page_person_add_name_dialog_hint": "名前",
|
"search_page_person_add_name_dialog_hint": "Name",
|
||||||
"search_page_person_add_name_dialog_save": "セーブ",
|
"search_page_person_add_name_dialog_save": "Save",
|
||||||
"search_page_person_add_name_dialog_title": "名前を追加",
|
"search_page_person_add_name_dialog_title": "Add a name",
|
||||||
"search_page_person_add_name_subtitle": "名前で検索して高速に探す",
|
"search_page_person_add_name_subtitle": "Find them fast by name with search",
|
||||||
"search_page_person_add_name_title": "名前を追加",
|
"search_page_person_add_name_title": "Add a name",
|
||||||
"search_page_person_edit_name": "名前を変更",
|
"search_page_person_edit_name": "Edit name",
|
||||||
"search_page_places": "撮影地",
|
"search_page_places": "撮影地",
|
||||||
"search_page_recently_added": "最近追加",
|
"search_page_recently_added": "最近追加",
|
||||||
"search_page_screenshots": "スクリーンショット",
|
"search_page_screenshots": "スクリーンショット",
|
||||||
@@ -342,7 +341,7 @@
|
|||||||
"search_page_videos": "ビデオ",
|
"search_page_videos": "ビデオ",
|
||||||
"search_page_view_all_button": "すべて表示",
|
"search_page_view_all_button": "すべて表示",
|
||||||
"search_page_your_activity": "アクティビティ",
|
"search_page_your_activity": "アクティビティ",
|
||||||
"search_page_your_map": "あなたのマップ",
|
"search_page_your_map": "Your Map",
|
||||||
"search_result_page_new_search_hint": "検索",
|
"search_result_page_new_search_hint": "検索",
|
||||||
"search_suggestion_list_smart_search_hint_1": "スマート検索はデフォルトでオンになっています。メタデータで検索を行う場合:",
|
"search_suggestion_list_smart_search_hint_1": "スマート検索はデフォルトでオンになっています。メタデータで検索を行う場合:",
|
||||||
"search_suggestion_list_smart_search_hint_2": "m:単語",
|
"search_suggestion_list_smart_search_hint_2": "m:単語",
|
||||||
@@ -350,7 +349,7 @@
|
|||||||
"select_user_for_sharing_page_err_album": "アルバム作成に失敗",
|
"select_user_for_sharing_page_err_album": "アルバム作成に失敗",
|
||||||
"select_user_for_sharing_page_share_suggestions": "ユーザ一覧",
|
"select_user_for_sharing_page_share_suggestions": "ユーザ一覧",
|
||||||
"server_info_box_app_version": "アプリVer.",
|
"server_info_box_app_version": "アプリVer.",
|
||||||
"server_info_box_latest_release": "最新バージョン",
|
"server_info_box_latest_release": "Latest Version",
|
||||||
"server_info_box_server_url": " サーバのURL",
|
"server_info_box_server_url": " サーバのURL",
|
||||||
"server_info_box_server_version": "サーバーVer.",
|
"server_info_box_server_version": "サーバーVer.",
|
||||||
"setting_image_viewer_help": "写真をタップするとサムネイル・中画質(要設定)・オリジナル(要設定)の順に読み込みます",
|
"setting_image_viewer_help": "写真をタップするとサムネイル・中画質(要設定)・オリジナル(要設定)の順に読み込みます",
|
||||||
@@ -376,66 +375,66 @@
|
|||||||
"share_add_photos": "写真を追加",
|
"share_add_photos": "写真を追加",
|
||||||
"share_add_title": "タイトルを追加",
|
"share_add_title": "タイトルを追加",
|
||||||
"share_create_album": "アルバムを作成",
|
"share_create_album": "アルバムを作成",
|
||||||
"shared_album_activities_input_disable": "コメントはオフになってます",
|
"shared_album_activities_input_disable": "Comment is disabled",
|
||||||
"shared_album_activities_input_hint": "何か書き込みましょう",
|
"shared_album_activities_input_hint": "Say something",
|
||||||
"shared_album_activity_remove_content": "このアクティビティを削除しますか",
|
"shared_album_activity_remove_content": "Do you want to delete this activity?",
|
||||||
"shared_album_activity_remove_title": "アクティビティを削除します",
|
"shared_album_activity_remove_title": "Delete Activity",
|
||||||
"shared_album_activity_setting_subtitle": "他のユーザーの返信を許可する",
|
"shared_album_activity_setting_subtitle": "Let others respond",
|
||||||
"shared_album_activity_setting_title": "お気に入りとコメント",
|
"shared_album_activity_setting_title": "Comments & likes",
|
||||||
"shared_album_section_people_action_error": "アルバムからの退出に失敗",
|
"shared_album_section_people_action_error": "Error leaving/removing from album",
|
||||||
"shared_album_section_people_action_leave": "ユーザーをアルバムから退出",
|
"shared_album_section_people_action_leave": "Remove user from album",
|
||||||
"shared_album_section_people_action_remove_user": "ユーザーをアルバムから退出",
|
"shared_album_section_people_action_remove_user": "Remove user from album",
|
||||||
"shared_album_section_people_owner_label": "オーナー",
|
"shared_album_section_people_owner_label": "Owner",
|
||||||
"shared_album_section_people_title": "ピープル",
|
"shared_album_section_people_title": "PEOPLE",
|
||||||
"share_dialog_preparing": "準備中",
|
"share_dialog_preparing": "準備中",
|
||||||
"shared_link_app_bar_title": "共有リンク",
|
"shared_link_app_bar_title": "共有リンク",
|
||||||
"shared_link_clipboard_copied_massage": "クリップボードにコピーしました",
|
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
||||||
"shared_link_clipboard_text": "リンク: {}\nパスワード: {}",
|
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
|
||||||
"shared_link_create_app_bar_title": "共有リンクを作る",
|
"shared_link_create_app_bar_title": "共有リンクを作る",
|
||||||
"shared_link_create_error": "共有用のリンク作成時にエラーが発生しました",
|
"shared_link_create_error": "Error while creating shared link",
|
||||||
"shared_link_create_info": "誰でも写真を見れるようにする",
|
"shared_link_create_info": "Let anyone with the link see the selected photo(s)",
|
||||||
"shared_link_create_submit_button": "リンクを作る",
|
"shared_link_create_submit_button": "リンクを作る",
|
||||||
"shared_link_edit_allow_download": "写真のダウンロードの許可",
|
"shared_link_edit_allow_download": "Allow public user to download",
|
||||||
"shared_link_edit_allow_upload": "写真のアップロードを許可",
|
"shared_link_edit_allow_upload": "Allow public user to upload",
|
||||||
"shared_link_edit_app_bar_title": " リンクを編集する",
|
"shared_link_edit_app_bar_title": " リンクを編集する",
|
||||||
"shared_link_edit_change_expiry": "有効期限を変更",
|
"shared_link_edit_change_expiry": "Change expiration time",
|
||||||
"shared_link_edit_description": "概要欄",
|
"shared_link_edit_description": " デスクリプション ",
|
||||||
"shared_link_edit_description_hint": "概要を追加",
|
"shared_link_edit_description_hint": "Enter the share description",
|
||||||
"shared_link_edit_expire_after": "有効期限は",
|
"shared_link_edit_expire_after": "Expire after",
|
||||||
"shared_link_edit_expire_after_option_day": "1日",
|
"shared_link_edit_expire_after_option_day": "1 day",
|
||||||
"shared_link_edit_expire_after_option_days": "{}日",
|
"shared_link_edit_expire_after_option_days": "{} days",
|
||||||
"shared_link_edit_expire_after_option_hour": "1時間",
|
"shared_link_edit_expire_after_option_hour": "1 hour",
|
||||||
"shared_link_edit_expire_after_option_hours": "{}時間",
|
"shared_link_edit_expire_after_option_hours": "{} hours",
|
||||||
"shared_link_edit_expire_after_option_minute": "1分",
|
"shared_link_edit_expire_after_option_minute": "1 minute",
|
||||||
"shared_link_edit_expire_after_option_minutes": "{}分",
|
"shared_link_edit_expire_after_option_minutes": "{} minutes",
|
||||||
"shared_link_edit_expire_after_option_never": "有効期限なし",
|
"shared_link_edit_expire_after_option_never": "Never",
|
||||||
"shared_link_edit_password": " パスワード",
|
"shared_link_edit_password": " パスワード",
|
||||||
"shared_link_edit_password_hint": "共有パスワードを入力する",
|
"shared_link_edit_password_hint": "共有パスワードを入力する",
|
||||||
"shared_link_edit_show_meta": " メタデータを見る",
|
"shared_link_edit_show_meta": " メタデータを見る",
|
||||||
"shared_link_edit_submit_button": "リンクをアップデートする",
|
"shared_link_edit_submit_button": "リンクをアップデートする",
|
||||||
"shared_link_empty": "共有リンクはありません ",
|
"shared_link_empty": "共有リンクはありません ",
|
||||||
"shared_link_error_server_url_fetch": "サーバーのURLがゲットできません",
|
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
|
||||||
"shared_link_expired": "有効期限が切れました",
|
"shared_link_expired": "Expired",
|
||||||
"shared_link_expires_day": "{}日間で切れます",
|
"shared_link_expires_day": "Expires in {} day",
|
||||||
"shared_link_expires_days": "{}日間で有効期限が切れます",
|
"shared_link_expires_days": "Expires in {} days",
|
||||||
"shared_link_expires_hour": "{}時間で切れます",
|
"shared_link_expires_hour": "Expires in {} hour",
|
||||||
"shared_link_expires_hours": "{}時間で有効期限が切れます",
|
"shared_link_expires_hours": "Expires in {} hours",
|
||||||
"shared_link_expires_minute": "{}分で切れます",
|
"shared_link_expires_minute": "Expires in {} minute",
|
||||||
"shared_link_expires_minutes": "{}分で切れます",
|
"shared_link_expires_minutes": "Expires in {} minutes",
|
||||||
"shared_link_expires_never": "有効期限はありません",
|
"shared_link_expires_never": "Expires ∞",
|
||||||
"shared_link_expires_second": "{}秒で切れます",
|
"shared_link_expires_second": "Expires in {} second",
|
||||||
"shared_link_expires_seconds": "{}秒で切れます",
|
"shared_link_expires_seconds": "Expires in {} seconds",
|
||||||
"shared_link_info_chip_download": "ダウンロード",
|
"shared_link_info_chip_download": "Download",
|
||||||
"shared_link_info_chip_metadata": "EXIF",
|
"shared_link_info_chip_metadata": "EXIF",
|
||||||
"shared_link_info_chip_upload": "アップロード",
|
"shared_link_info_chip_upload": "Upload",
|
||||||
"shared_link_manage_links": "共有済みのリンクを管理",
|
"shared_link_manage_links": "Manage Shared links",
|
||||||
"share_done": "完了",
|
"share_done": "Done",
|
||||||
"share_invite": "アルバムに招待",
|
"share_invite": "アルバムに招待",
|
||||||
"sharing_page_album": "共有アルバム",
|
"sharing_page_album": "共有アルバム",
|
||||||
"sharing_page_description": "共有アルバムを作成して同じネットワークにいる人たちに写真を共有",
|
"sharing_page_description": "共有アルバムを作成して同じネットワークにいる人たちに写真を共有",
|
||||||
"sharing_page_empty_list": "共有アルバムなし",
|
"sharing_page_empty_list": "共有アルバムなし",
|
||||||
"sharing_silver_appbar_create_shared_album": "共有アルバムを作成",
|
"sharing_silver_appbar_create_shared_album": "共有アルバムを作成",
|
||||||
"sharing_silver_appbar_shared_links": "共有リンク",
|
"sharing_silver_appbar_shared_links": "Shared links",
|
||||||
"sharing_silver_appbar_share_partner": "パートナーと共有",
|
"sharing_silver_appbar_share_partner": "パートナーと共有",
|
||||||
"tab_controller_nav_library": "ライブラリ",
|
"tab_controller_nav_library": "ライブラリ",
|
||||||
"tab_controller_nav_photos": "写真",
|
"tab_controller_nav_photos": "写真",
|
||||||
@@ -451,30 +450,30 @@
|
|||||||
"theme_setting_theme_title": "テーマ",
|
"theme_setting_theme_title": "テーマ",
|
||||||
"theme_setting_three_stage_loading_subtitle": "三段階読み込みを有効にするとパフォーマンスが改善する可能性がありますが、ネットワーク負荷が著しく増加します",
|
"theme_setting_three_stage_loading_subtitle": "三段階読み込みを有効にするとパフォーマンスが改善する可能性がありますが、ネットワーク負荷が著しく増加します",
|
||||||
"theme_setting_three_stage_loading_title": "三段階読み込みをオンにする",
|
"theme_setting_three_stage_loading_title": "三段階読み込みをオンにする",
|
||||||
"translated_text_options": "オプション",
|
"translated_text_options": "Options",
|
||||||
"trash_page_delete": "削除",
|
"trash_page_delete": "Delete",
|
||||||
"trash_page_delete_all": "全て削除",
|
"trash_page_delete_all": "Delete All",
|
||||||
"trash_page_empty_trash_btn": "コミ箱を空にする",
|
"trash_page_empty_trash_btn": "Empty trash",
|
||||||
"trash_page_empty_trash_dialog_content": "ゴミ箱を空にしますか?選択された項目は完全に削除されます。この操作は取り消せません",
|
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich",
|
||||||
"trash_page_empty_trash_dialog_ok": "了解",
|
"trash_page_empty_trash_dialog_ok": "Ok",
|
||||||
"trash_page_info": "ゴミ箱に移動したアイテムは{}日後に削除されます",
|
"trash_page_info": "Trashed items will be permanently deleted after {} days",
|
||||||
"trash_page_no_assets": "ゴミ箱は空です",
|
"trash_page_no_assets": "No trashed assets",
|
||||||
"trash_page_restore": "復元",
|
"trash_page_restore": "Restore",
|
||||||
"trash_page_restore_all": "全て復元",
|
"trash_page_restore_all": "Restore All",
|
||||||
"trash_page_select_assets_btn": "項目を選択",
|
"trash_page_select_assets_btn": "Select assets",
|
||||||
"trash_page_select_btn": "選択",
|
"trash_page_select_btn": "Select",
|
||||||
"trash_page_title": "削除({})",
|
"trash_page_title": "Trash ({})",
|
||||||
"upload_dialog_cancel": "キャンセル",
|
"upload_dialog_cancel": "Cancel",
|
||||||
"upload_dialog_info": "選択した項目のバックアップをしますか?",
|
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
||||||
"upload_dialog_ok": "アップロード",
|
"upload_dialog_ok": "Upload",
|
||||||
"upload_dialog_title": "アップロード",
|
"upload_dialog_title": "Upload Asset",
|
||||||
"version_announcement_overlay_ack": "了解",
|
"version_announcement_overlay_ack": "了解",
|
||||||
"version_announcement_overlay_release_notes": "更新情報",
|
"version_announcement_overlay_release_notes": "更新情報",
|
||||||
"version_announcement_overlay_text_1": "こんにちは、またはこんばんは!新しい",
|
"version_announcement_overlay_text_1": "こんにちは、またはこんばんは!新しい",
|
||||||
"version_announcement_overlay_text_2": "のバージョンが公開中です。",
|
"version_announcement_overlay_text_2": "のバージョンが公開中です。",
|
||||||
"version_announcement_overlay_text_3": "を確認してみてください。docker-composeや.envファイルが最新の状態に更新されているか、特にWatchTowerなどのツールを使ってDockerイメージを自動アップデートしてる人は確認してください。",
|
"version_announcement_overlay_text_3": "を確認してみてください。docker-composeや.envファイルが最新の状態に更新されているか、特にWatchTowerなどのツールを使ってDockerイメージを自動アップデートしてる人は確認してください。",
|
||||||
"version_announcement_overlay_title": "サーバーの新バージョンリリース\uD83C\uDF89",
|
"version_announcement_overlay_title": "サーバーの新バージョンリリース\uD83C\uDF89",
|
||||||
"viewer_remove_from_stack": "スタックから外す",
|
"viewer_remove_from_stack": "Remove from Stack",
|
||||||
"viewer_stack_use_as_main_asset": "メインの画像として使用する",
|
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||||
"viewer_unstack": "スタックを解除"
|
"viewer_unstack": "Un-Stack"
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "상세정보",
|
"exif_bottom_sheet_details": "상세정보",
|
||||||
"exif_bottom_sheet_location": "위치",
|
"exif_bottom_sheet_location": "위치",
|
||||||
"exif_bottom_sheet_location_add": "위치 지정",
|
"exif_bottom_sheet_location_add": "위치 지정",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "진행중",
|
"experimental_settings_new_asset_list_subtitle": "진행중",
|
||||||
"experimental_settings_new_asset_list_title": "실험적 사진 그리드 적용",
|
"experimental_settings_new_asset_list_title": "실험적 사진 그리드 적용",
|
||||||
"experimental_settings_subtitle": "문제시 책임지지 않습니다!",
|
"experimental_settings_subtitle": "문제시 책임지지 않습니다!",
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "INFORMĀCIJA",
|
"exif_bottom_sheet_details": "INFORMĀCIJA",
|
||||||
"exif_bottom_sheet_location": "ATRAŠANĀS VIETA",
|
"exif_bottom_sheet_location": "ATRAŠANĀS VIETA",
|
||||||
"exif_bottom_sheet_location_add": "Add a location",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Izstrādes posmā",
|
"experimental_settings_new_asset_list_subtitle": "Izstrādes posmā",
|
||||||
"experimental_settings_new_asset_list_title": "Iespējot eksperimentālo fotorežģi",
|
"experimental_settings_new_asset_list_title": "Iespējot eksperimentālo fotorežģi",
|
||||||
"experimental_settings_subtitle": "Izmanto uzņemoties risku!",
|
"experimental_settings_subtitle": "Izmanto uzņemoties risku!",
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "DETAILS",
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
"exif_bottom_sheet_location": "LOCATION",
|
"exif_bottom_sheet_location": "LOCATION",
|
||||||
"exif_bottom_sheet_location_add": "Add a location",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||||
"experimental_settings_subtitle": "Use at your own risk!",
|
"experimental_settings_subtitle": "Use at your own risk!",
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
"app_bar_signout_dialog_title": "Logg ut",
|
"app_bar_signout_dialog_title": "Logg ut",
|
||||||
"archive_page_no_archived_assets": "Ingen arkiverte objekter funnet",
|
"archive_page_no_archived_assets": "Ingen arkiverte objekter funnet",
|
||||||
"archive_page_title": "Arkiv ({})",
|
"archive_page_title": "Arkiv ({})",
|
||||||
"asset_action_delete_err_read_only": "Kan ikke slette objekt(er) med kun lese-rettighet, hopper over",
|
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
|
||||||
"asset_action_share_err_offline": "Kan ikke hente offline objekt(er), hopper over",
|
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk bildeorganisering",
|
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk bildeorganisering",
|
||||||
"asset_list_layout_settings_group_automatically": "Automatisk",
|
"asset_list_layout_settings_group_automatically": "Automatisk",
|
||||||
"asset_list_layout_settings_group_by": "Grupper bilder etter",
|
"asset_list_layout_settings_group_by": "Grupper bilder etter",
|
||||||
@@ -142,15 +142,15 @@
|
|||||||
"control_bottom_app_bar_archive": "Arkiver",
|
"control_bottom_app_bar_archive": "Arkiver",
|
||||||
"control_bottom_app_bar_create_new_album": "Lag nytt album",
|
"control_bottom_app_bar_create_new_album": "Lag nytt album",
|
||||||
"control_bottom_app_bar_delete": "Slett",
|
"control_bottom_app_bar_delete": "Slett",
|
||||||
"control_bottom_app_bar_delete_from_immich": "Slett fra Immich",
|
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||||
"control_bottom_app_bar_delete_from_local": "Slett fra enhet",
|
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||||
"control_bottom_app_bar_edit_location": "Endre lokasjon",
|
"control_bottom_app_bar_edit_location": "Endre lokasjon",
|
||||||
"control_bottom_app_bar_edit_time": "Endre Dato og tid",
|
"control_bottom_app_bar_edit_time": "Endre Dato og tid",
|
||||||
"control_bottom_app_bar_favorite": "Favoritt",
|
"control_bottom_app_bar_favorite": "Favoritt",
|
||||||
"control_bottom_app_bar_share": "Del",
|
"control_bottom_app_bar_share": "Del",
|
||||||
"control_bottom_app_bar_share_to": "Del til",
|
"control_bottom_app_bar_share_to": "Del til",
|
||||||
"control_bottom_app_bar_stack": "Stable",
|
"control_bottom_app_bar_stack": "Stable",
|
||||||
"control_bottom_app_bar_trash_from_immich": "Flytt til søppelkasse",
|
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
|
||||||
"control_bottom_app_bar_unarchive": "Fjern fra arkiv",
|
"control_bottom_app_bar_unarchive": "Fjern fra arkiv",
|
||||||
"control_bottom_app_bar_unfavorite": "Fjern favoritt",
|
"control_bottom_app_bar_unfavorite": "Fjern favoritt",
|
||||||
"control_bottom_app_bar_upload": "Last opp",
|
"control_bottom_app_bar_upload": "Last opp",
|
||||||
@@ -165,15 +165,15 @@
|
|||||||
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||||
"date_format": "E, LLL d, y • h:mm a",
|
"date_format": "E, LLL d, y • h:mm a",
|
||||||
"delete_dialog_alert": "Disse objektene vil bli slettet permanent fra Immich og fra enheten din",
|
"delete_dialog_alert": "Disse objektene vil bli slettet permanent fra Immich og fra enheten din",
|
||||||
"delete_dialog_alert_local": "Disse objektene vil bli permanent slettet fra enheten din, men vil fortsatt være tilgjengelige fra Immich serveren",
|
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
|
||||||
"delete_dialog_alert_local_non_backed_up": "Noen av objektene er ikke sikkerhetskopiert til Immich og vil bli permanent fjernet fra enheten din",
|
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
|
||||||
"delete_dialog_alert_remote": "Disse objektene vil bli permanent slettet fra Immich serveren",
|
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
|
||||||
"delete_dialog_cancel": "Avbryt",
|
"delete_dialog_cancel": "Avbryt",
|
||||||
"delete_dialog_ok": "Slett",
|
"delete_dialog_ok": "Slett",
|
||||||
"delete_dialog_ok_force": "Slett uansett",
|
"delete_dialog_ok_force": "Delete Anyway",
|
||||||
"delete_dialog_title": "Slett permanent",
|
"delete_dialog_title": "Slett permanent",
|
||||||
"delete_local_dialog_ok_backed_up_only": "Slett kun sikkerhetskopierte objekter",
|
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||||
"delete_local_dialog_ok_force": "Slett uansett",
|
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||||
"delete_shared_link_dialog_content": "Er du sikker på at du vil slette denne delte linken?",
|
"delete_shared_link_dialog_content": "Er du sikker på at du vil slette denne delte linken?",
|
||||||
"delete_shared_link_dialog_title": "Slett delt link",
|
"delete_shared_link_dialog_title": "Slett delt link",
|
||||||
"description_input_hint_text": "Legg til beskrivelse ...",
|
"description_input_hint_text": "Legg til beskrivelse ...",
|
||||||
@@ -185,7 +185,6 @@
|
|||||||
"exif_bottom_sheet_details": "DETALJER",
|
"exif_bottom_sheet_details": "DETALJER",
|
||||||
"exif_bottom_sheet_location": "PLASSERING",
|
"exif_bottom_sheet_location": "PLASSERING",
|
||||||
"exif_bottom_sheet_location_add": "Legg til lokasjon",
|
"exif_bottom_sheet_location_add": "Legg til lokasjon",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Under utvikling",
|
"experimental_settings_new_asset_list_subtitle": "Under utvikling",
|
||||||
"experimental_settings_new_asset_list_title": "Aktiver eksperimentell rutenettsvisning",
|
"experimental_settings_new_asset_list_title": "Aktiver eksperimentell rutenettsvisning",
|
||||||
"experimental_settings_subtitle": "Bruk på egen risiko!",
|
"experimental_settings_subtitle": "Bruk på egen risiko!",
|
||||||
@@ -200,7 +199,7 @@
|
|||||||
"home_page_archive_err_partner": "Kan ikke arkivere partnerobjekter, hopper over",
|
"home_page_archive_err_partner": "Kan ikke arkivere partnerobjekter, hopper over",
|
||||||
"home_page_building_timeline": "Genererer tidslinjen",
|
"home_page_building_timeline": "Genererer tidslinjen",
|
||||||
"home_page_delete_err_partner": "Kan ikke slette partnerobjekter, hopper over",
|
"home_page_delete_err_partner": "Kan ikke slette partnerobjekter, hopper over",
|
||||||
"home_page_delete_remote_err_local": "Lokale objekter i fjernslettingsvalgene, hopper over",
|
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
|
||||||
"home_page_favorite_err_local": "Kan ikke sette favoritt på lokale objekter enda, hopper over",
|
"home_page_favorite_err_local": "Kan ikke sette favoritt på lokale objekter enda, hopper over",
|
||||||
"home_page_favorite_err_partner": "Kan ikke merke partnerobjekter som favoritt enda, hopper over",
|
"home_page_favorite_err_partner": "Kan ikke merke partnerobjekter som favoritt enda, hopper over",
|
||||||
"home_page_first_time_notice": "Hvis dette er første gangen du benytter appen, velg et album (eller flere) for sikkerhetskopiering, slik at tidslinjen kan fylles med dine bilder og videoer.",
|
"home_page_first_time_notice": "Hvis dette er første gangen du benytter appen, velg et album (eller flere) for sikkerhetskopiering, slik at tidslinjen kan fylles med dine bilder og videoer.",
|
||||||
@@ -276,12 +275,12 @@
|
|||||||
"map_settings_include_show_archived": "Inkluder arkiverte",
|
"map_settings_include_show_archived": "Inkluder arkiverte",
|
||||||
"map_settings_only_relative_range": "Datoområde",
|
"map_settings_only_relative_range": "Datoområde",
|
||||||
"map_settings_only_show_favorites": "Vis kun favoritter",
|
"map_settings_only_show_favorites": "Vis kun favoritter",
|
||||||
"map_settings_theme_settings": "Karttema",
|
"map_settings_theme_settings": "Map Theme",
|
||||||
"map_zoom_to_see_photos": "Zoom ut for å se bilder",
|
"map_zoom_to_see_photos": "Zoom ut for å se bilder",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"motion_photos_page_title": "Bevegelige bilder",
|
"motion_photos_page_title": "Bevegelige bilder",
|
||||||
"multiselect_grid_edit_date_time_err_read_only": "Kan ikke endre dato på objekt(er) med kun lese-rettigheter, hopper over",
|
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||||
"multiselect_grid_edit_gps_err_read_only": "Kan ikke endre lokasjon på objekt(er) med kun lese-rettigheter, hopper over",
|
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
||||||
"notification_permission_dialog_cancel": "Avbryt",
|
"notification_permission_dialog_cancel": "Avbryt",
|
||||||
"notification_permission_dialog_content": "For å aktivere notifikasjoner, gå til Innstillinger og velg tillat.",
|
"notification_permission_dialog_content": "For å aktivere notifikasjoner, gå til Innstillinger og velg tillat.",
|
||||||
"notification_permission_dialog_settings": "Innstillinger",
|
"notification_permission_dialog_settings": "Innstillinger",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"action_common_cancel": "Annuleren",
|
"action_common_cancel": "Annuleren",
|
||||||
"action_common_update": "Bijwerken",
|
"action_common_update": "Updaten",
|
||||||
"add_to_album_bottom_sheet_added": "Toegevoegd aan {album}",
|
"add_to_album_bottom_sheet_added": "Toegevoegd aan {album}",
|
||||||
"add_to_album_bottom_sheet_already_exists": "Staat al in {album}",
|
"add_to_album_bottom_sheet_already_exists": "Staat al in {album}",
|
||||||
"advanced_settings_log_level_title": "Log niveau: {}",
|
"advanced_settings_log_level_title": "Log niveau: {}",
|
||||||
@@ -26,17 +26,17 @@
|
|||||||
"album_viewer_appbar_share_err_title": "Albumtitel wijzigen mislukt",
|
"album_viewer_appbar_share_err_title": "Albumtitel wijzigen mislukt",
|
||||||
"album_viewer_appbar_share_leave": "Verlaat album",
|
"album_viewer_appbar_share_leave": "Verlaat album",
|
||||||
"album_viewer_appbar_share_remove": "Verwijder uit album",
|
"album_viewer_appbar_share_remove": "Verwijder uit album",
|
||||||
"album_viewer_appbar_share_to": "Delen met",
|
"album_viewer_appbar_share_to": "Deel Naar",
|
||||||
"album_viewer_page_share_add_users": "Gebruikers toevoegen",
|
"album_viewer_page_share_add_users": "Gebruikers toevoegen",
|
||||||
"all_people_page_title": "Personen",
|
"all_people_page_title": "Personen",
|
||||||
"all_videos_page_title": "Video's",
|
"all_videos_page_title": "Video's",
|
||||||
"app_bar_signout_dialog_content": "Weet je zeker dat je wilt uitloggen?",
|
"app_bar_signout_dialog_content": "Weet je zeker dat je je wilt afmelden?",
|
||||||
"app_bar_signout_dialog_ok": "Ja",
|
"app_bar_signout_dialog_ok": "Ja",
|
||||||
"app_bar_signout_dialog_title": "Log uit",
|
"app_bar_signout_dialog_title": "Log uit",
|
||||||
"archive_page_no_archived_assets": "Geen gearchiveerde assets gevonden",
|
"archive_page_no_archived_assets": "Geen gearchiveerde assets gevonden",
|
||||||
"archive_page_title": "Archief ({})",
|
"archive_page_title": "Archief ({})",
|
||||||
"asset_action_delete_err_read_only": "Kan alleen-lezen asset(s) niet verwijderen, overslaan",
|
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
|
||||||
"asset_action_share_err_offline": "Kan offline asset(s) niet ophalen, overslaan",
|
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamische layout",
|
"asset_list_layout_settings_dynamic_layout_title": "Dynamische layout",
|
||||||
"asset_list_layout_settings_group_automatically": "Automatisch",
|
"asset_list_layout_settings_group_automatically": "Automatisch",
|
||||||
"asset_list_layout_settings_group_by": "Groupeer assets per",
|
"asset_list_layout_settings_group_by": "Groupeer assets per",
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"backup_err_only_album": "Kan het enige album niet verwijderen",
|
"backup_err_only_album": "Kan het enige album niet verwijderen",
|
||||||
"backup_info_card_assets": "assets",
|
"backup_info_card_assets": "assets",
|
||||||
"backup_manual_cancelled": "Geannuleerd",
|
"backup_manual_cancelled": "Geannuleerd",
|
||||||
"backup_manual_failed": "Mislukt",
|
"backup_manual_failed": "Gefaald",
|
||||||
"backup_manual_in_progress": "Het uploaden is al bezig. Probeer het na een tijdje",
|
"backup_manual_in_progress": "Het uploaden is al bezig. Probeer het na een tijdje",
|
||||||
"backup_manual_success": "Succes",
|
"backup_manual_success": "Succes",
|
||||||
"backup_manual_title": "Uploadstatus",
|
"backup_manual_title": "Uploadstatus",
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
"cache_settings_clear_cache_button_title": "Wist de cache van de app. Dit zal de presentaties van de app aanzienlijk beïnvloeden totdat de cache opnieuw is opgebouwd.",
|
"cache_settings_clear_cache_button_title": "Wist de cache van de app. Dit zal de presentaties van de app aanzienlijk beïnvloeden totdat de cache opnieuw is opgebouwd.",
|
||||||
"cache_settings_duplicated_assets_clear_button": "MAAK VRIJ",
|
"cache_settings_duplicated_assets_clear_button": "MAAK VRIJ",
|
||||||
"cache_settings_duplicated_assets_subtitle": "Foto's en video's op de zwarte lijst van de app",
|
"cache_settings_duplicated_assets_subtitle": "Foto's en video's op de zwarte lijst van de app",
|
||||||
"cache_settings_duplicated_assets_title": "Gedupliceerde assets ({})",
|
"cache_settings_duplicated_assets_title": "Gedupliceerde Assets ({})",
|
||||||
"cache_settings_image_cache_size": "Grootte afbeeldingscache ({} assets)",
|
"cache_settings_image_cache_size": "Grootte afbeeldingscache ({} assets)",
|
||||||
"cache_settings_statistics_album": "Bibliotheekthumbnails",
|
"cache_settings_statistics_album": "Bibliotheekthumbnails",
|
||||||
"cache_settings_statistics_assets": "{} assets ({})",
|
"cache_settings_statistics_assets": "{} assets ({})",
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
"cache_settings_subtitle": "Beheer het cachegedrag van de Immich app",
|
"cache_settings_subtitle": "Beheer het cachegedrag van de Immich app",
|
||||||
"cache_settings_thumbnail_size": "Thumbnail-cachegrootte ({} assets)",
|
"cache_settings_thumbnail_size": "Thumbnail-cachegrootte ({} assets)",
|
||||||
"cache_settings_tile_subtitle": "Beheer het gedrag van lokale opslag",
|
"cache_settings_tile_subtitle": "Beheer het gedrag van lokale opslag",
|
||||||
"cache_settings_tile_title": "Lokale opslag",
|
"cache_settings_tile_title": "Lokale Opslag",
|
||||||
"cache_settings_title": "Cache-instellingen",
|
"cache_settings_title": "Cache-instellingen",
|
||||||
"change_password_form_confirm_password": "Bevestig wachtwoord",
|
"change_password_form_confirm_password": "Bevestig wachtwoord",
|
||||||
"change_password_form_description": "Hallo {name},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.",
|
"change_password_form_description": "Hallo {name},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.",
|
||||||
@@ -142,15 +142,15 @@
|
|||||||
"control_bottom_app_bar_archive": "Archiveren",
|
"control_bottom_app_bar_archive": "Archiveren",
|
||||||
"control_bottom_app_bar_create_new_album": "Nieuw album maken",
|
"control_bottom_app_bar_create_new_album": "Nieuw album maken",
|
||||||
"control_bottom_app_bar_delete": "Verwijderen",
|
"control_bottom_app_bar_delete": "Verwijderen",
|
||||||
"control_bottom_app_bar_delete_from_immich": "Verwijderen van Immich",
|
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||||
"control_bottom_app_bar_delete_from_local": "Verwijderen van apparaat",
|
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||||
"control_bottom_app_bar_edit_location": "Locatie bewerken",
|
"control_bottom_app_bar_edit_location": "Locatie Bewerken",
|
||||||
"control_bottom_app_bar_edit_time": "Datum & tijd bewerken",
|
"control_bottom_app_bar_edit_time": "Datum & Tijd Bewerken",
|
||||||
"control_bottom_app_bar_favorite": "Favoriet",
|
"control_bottom_app_bar_favorite": "Favoriet",
|
||||||
"control_bottom_app_bar_share": "Delen",
|
"control_bottom_app_bar_share": "Delen",
|
||||||
"control_bottom_app_bar_share_to": "Delen met",
|
"control_bottom_app_bar_share_to": "Deel Naar",
|
||||||
"control_bottom_app_bar_stack": "Stapel",
|
"control_bottom_app_bar_stack": "Stapel",
|
||||||
"control_bottom_app_bar_trash_from_immich": "Verplaatsen naar prullenbak",
|
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
|
||||||
"control_bottom_app_bar_unarchive": "Herstellen",
|
"control_bottom_app_bar_unarchive": "Herstellen",
|
||||||
"control_bottom_app_bar_unfavorite": "Onfavoriet",
|
"control_bottom_app_bar_unfavorite": "Onfavoriet",
|
||||||
"control_bottom_app_bar_upload": "Uploaden",
|
"control_bottom_app_bar_upload": "Uploaden",
|
||||||
@@ -165,27 +165,26 @@
|
|||||||
"daily_title_text_date_year": "E dd MMM yyyy",
|
"daily_title_text_date_year": "E dd MMM yyyy",
|
||||||
"date_format": "E d LLL y • H:mm",
|
"date_format": "E d LLL y • H:mm",
|
||||||
"delete_dialog_alert": "Deze items zullen permanent verwijderd worden van Immich en je apparaat",
|
"delete_dialog_alert": "Deze items zullen permanent verwijderd worden van Immich en je apparaat",
|
||||||
"delete_dialog_alert_local": "Deze items worden permanent verwijderd van je apparaat, maar blijven beschikbaar op de Immich server",
|
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
|
||||||
"delete_dialog_alert_local_non_backed_up": "Van sommige items is geen back-up gemaakt in Immich en zullen permanent van je apparaat worden verwijderd",
|
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
|
||||||
"delete_dialog_alert_remote": "Deze items worden permanent verwijderd van de Immich server",
|
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
|
||||||
"delete_dialog_cancel": "Annuleren",
|
"delete_dialog_cancel": "Annuleren",
|
||||||
"delete_dialog_ok": "Verwijderen",
|
"delete_dialog_ok": "Verwijderen",
|
||||||
"delete_dialog_ok_force": "Toch verwijderen",
|
"delete_dialog_ok_force": "Delete Anyway",
|
||||||
"delete_dialog_title": "Permanent verwijderen",
|
"delete_dialog_title": "Permanent verwijderen",
|
||||||
"delete_local_dialog_ok_backed_up_only": "Verwijder alleen met back-up",
|
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||||
"delete_local_dialog_ok_force": "Toch verwijderen",
|
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||||
"delete_shared_link_dialog_content": "Weet je zeker dat je deze gedeelde link wilt verwijderen?",
|
"delete_shared_link_dialog_content": "Weet je zeker dat je deze gedeelde link wilt verwijderen?",
|
||||||
"delete_shared_link_dialog_title": "Verwijder gedeelde link",
|
"delete_shared_link_dialog_title": "Verwijder Gedeelde Link",
|
||||||
"description_input_hint_text": "Beschrijving toevoegen...",
|
"description_input_hint_text": "Beschrijving toevoegen...",
|
||||||
"description_input_submit_error": "Beschrijving bijwerken mislukt, controleer het logboek voor meer details",
|
"description_input_submit_error": "Beschrijving bijwerken mislukt, controleer het logboek voor meer details",
|
||||||
"edit_date_time_dialog_date_time": "Datum en tijd",
|
"edit_date_time_dialog_date_time": "Datum en Tijd",
|
||||||
"edit_date_time_dialog_timezone": "Tijdzone",
|
"edit_date_time_dialog_timezone": "Tijdzone",
|
||||||
"edit_location_dialog_title": "Locatie",
|
"edit_location_dialog_title": "Locatie",
|
||||||
"exif_bottom_sheet_description": "Beschrijving toevoegen...",
|
"exif_bottom_sheet_description": "Beschrijving toevoegen...",
|
||||||
"exif_bottom_sheet_details": "DETAILS",
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
"exif_bottom_sheet_location": "LOCATIE",
|
"exif_bottom_sheet_location": "LOCATIE",
|
||||||
"exif_bottom_sheet_location_add": "Locatie toevoegen",
|
"exif_bottom_sheet_location_add": "Locatie toevoegen",
|
||||||
"exif_bottom_sheet_people": "MENSEN",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Werk in uitvoering",
|
"experimental_settings_new_asset_list_subtitle": "Werk in uitvoering",
|
||||||
"experimental_settings_new_asset_list_title": "Experimenteel fotoraster inschakelen",
|
"experimental_settings_new_asset_list_title": "Experimenteel fotoraster inschakelen",
|
||||||
"experimental_settings_subtitle": "Gebruik op eigen risico!",
|
"experimental_settings_subtitle": "Gebruik op eigen risico!",
|
||||||
@@ -200,7 +199,7 @@
|
|||||||
"home_page_archive_err_partner": "Partner assets kunnen niet gearchiveerd worden, overslaan",
|
"home_page_archive_err_partner": "Partner assets kunnen niet gearchiveerd worden, overslaan",
|
||||||
"home_page_building_timeline": "Tijdlijn opbouwen",
|
"home_page_building_timeline": "Tijdlijn opbouwen",
|
||||||
"home_page_delete_err_partner": "Partner assets kunnen niet verwijderd worden, overslaan",
|
"home_page_delete_err_partner": "Partner assets kunnen niet verwijderd worden, overslaan",
|
||||||
"home_page_delete_remote_err_local": "Lokale assets staan in verwijder selectie externe assets, overslaan",
|
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
|
||||||
"home_page_favorite_err_local": "Lokale assets kunnen nog niet als favoriet worden aangemerkt, overslaan",
|
"home_page_favorite_err_local": "Lokale assets kunnen nog niet als favoriet worden aangemerkt, overslaan",
|
||||||
"home_page_favorite_err_partner": "Partner assets kunnen nog niet ge-favoriet worden, overslaan",
|
"home_page_favorite_err_partner": "Partner assets kunnen nog niet ge-favoriet worden, overslaan",
|
||||||
"home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album.",
|
"home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album.",
|
||||||
@@ -260,10 +259,10 @@
|
|||||||
"map_location_dialog_yes": "Ja",
|
"map_location_dialog_yes": "Ja",
|
||||||
"map_location_picker_page_use_location": "Gebruik deze locatie",
|
"map_location_picker_page_use_location": "Gebruik deze locatie",
|
||||||
"map_location_service_disabled_content": "Locatie service moet ingeschakeld zijn om assets van je huidige locatie weer te geven. Wil je het nu inschakelen?",
|
"map_location_service_disabled_content": "Locatie service moet ingeschakeld zijn om assets van je huidige locatie weer te geven. Wil je het nu inschakelen?",
|
||||||
"map_location_service_disabled_title": "Locatie service uitgeschakeld",
|
"map_location_service_disabled_title": "Locatie Service uitgeschakeld",
|
||||||
"map_no_assets_in_bounds": "Geen foto's in dit gebied",
|
"map_no_assets_in_bounds": "Geen foto's in dit gebied",
|
||||||
"map_no_location_permission_content": "Locatie toestemming is nodig om assets van je huidige locatie weer te geven. Wil je het nu toestaan?",
|
"map_no_location_permission_content": "Locatie toestemming is nodig om assets van je huidige locatie weer te geven. Wil je het nu toestaan?",
|
||||||
"map_no_location_permission_title": "Locatie toestemming geweigerd",
|
"map_no_location_permission_title": "Locatie Toestemming geweigerd",
|
||||||
"map_settings_dark_mode": "Donkere modus",
|
"map_settings_dark_mode": "Donkere modus",
|
||||||
"map_settings_date_range_option_all": "Alle",
|
"map_settings_date_range_option_all": "Alle",
|
||||||
"map_settings_date_range_option_day": "Afgelopen 24 uur",
|
"map_settings_date_range_option_day": "Afgelopen 24 uur",
|
||||||
@@ -271,17 +270,17 @@
|
|||||||
"map_settings_date_range_option_year": "Afgelopen jaar",
|
"map_settings_date_range_option_year": "Afgelopen jaar",
|
||||||
"map_settings_date_range_option_years": "Afgelopen {} jaar",
|
"map_settings_date_range_option_years": "Afgelopen {} jaar",
|
||||||
"map_settings_dialog_cancel": "Annuleren",
|
"map_settings_dialog_cancel": "Annuleren",
|
||||||
"map_settings_dialog_save": "Opslaan",
|
"map_settings_dialog_save": "Sla op",
|
||||||
"map_settings_dialog_title": "Kaart Instellingen",
|
"map_settings_dialog_title": "Kaart Instellingen",
|
||||||
"map_settings_include_show_archived": "Toon gearchiveerde",
|
"map_settings_include_show_archived": "Weergeef Gearchiveerden",
|
||||||
"map_settings_only_relative_range": "Datum bereik",
|
"map_settings_only_relative_range": "Datum bereik",
|
||||||
"map_settings_only_show_favorites": "Toon enkel favorieten",
|
"map_settings_only_show_favorites": "Toon enkel favorieten",
|
||||||
"map_settings_theme_settings": "Kaart thema",
|
"map_settings_theme_settings": "Map Theme",
|
||||||
"map_zoom_to_see_photos": "Zoom uit om foto's te zien",
|
"map_zoom_to_see_photos": "Zoom uit om foto's te zien",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"motion_photos_page_title": "Bewegende foto's",
|
"motion_photos_page_title": "Bewegende foto's",
|
||||||
"multiselect_grid_edit_date_time_err_read_only": "Kan datum van alleen-lezen asset(s) niet wijzigen, overslaan",
|
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||||
"multiselect_grid_edit_gps_err_read_only": "Kan locatie van alleen-lezen asset(s) niet wijzigen, overslaan",
|
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
||||||
"notification_permission_dialog_cancel": "Annuleren",
|
"notification_permission_dialog_cancel": "Annuleren",
|
||||||
"notification_permission_dialog_content": "Om meldingen in te schakelen, ga naar Instellingen en selecteer toestaan.",
|
"notification_permission_dialog_content": "Om meldingen in te schakelen, ga naar Instellingen en selecteer toestaan.",
|
||||||
"notification_permission_dialog_settings": "Instellingen",
|
"notification_permission_dialog_settings": "Instellingen",
|
||||||
@@ -391,7 +390,7 @@
|
|||||||
"shared_link_app_bar_title": "Gedeelde links",
|
"shared_link_app_bar_title": "Gedeelde links",
|
||||||
"shared_link_clipboard_copied_massage": "Gekopieerd naar klembord",
|
"shared_link_clipboard_copied_massage": "Gekopieerd naar klembord",
|
||||||
"shared_link_clipboard_text": "Link: {}\nWachtwoord: {}",
|
"shared_link_clipboard_text": "Link: {}\nWachtwoord: {}",
|
||||||
"shared_link_create_app_bar_title": "Gedeelde link maken",
|
"shared_link_create_app_bar_title": "Link maken om te delen",
|
||||||
"shared_link_create_error": "Fout bij het maken van een gedeelde link",
|
"shared_link_create_error": "Fout bij het maken van een gedeelde link",
|
||||||
"shared_link_create_info": "Laat iedereen met de link de geselecteerde foto(s) zien",
|
"shared_link_create_info": "Laat iedereen met de link de geselecteerde foto(s) zien",
|
||||||
"shared_link_create_submit_button": "Link maken",
|
"shared_link_create_submit_button": "Link maken",
|
||||||
@@ -400,7 +399,7 @@
|
|||||||
"shared_link_edit_app_bar_title": "Bewerk link",
|
"shared_link_edit_app_bar_title": "Bewerk link",
|
||||||
"shared_link_edit_change_expiry": "Bewerk vervaltijd",
|
"shared_link_edit_change_expiry": "Bewerk vervaltijd",
|
||||||
"shared_link_edit_description": "Beschrijving",
|
"shared_link_edit_description": "Beschrijving",
|
||||||
"shared_link_edit_description_hint": "Voer beschrijving voor de gedeelde link in",
|
"shared_link_edit_description_hint": "Geef de deel beschrijving",
|
||||||
"shared_link_edit_expire_after": "Verval na",
|
"shared_link_edit_expire_after": "Verval na",
|
||||||
"shared_link_edit_expire_after_option_day": "1 dag",
|
"shared_link_edit_expire_after_option_day": "1 dag",
|
||||||
"shared_link_edit_expire_after_option_days": "{} dagen",
|
"shared_link_edit_expire_after_option_days": "{} dagen",
|
||||||
@@ -410,9 +409,9 @@
|
|||||||
"shared_link_edit_expire_after_option_minutes": "{} minuten",
|
"shared_link_edit_expire_after_option_minutes": "{} minuten",
|
||||||
"shared_link_edit_expire_after_option_never": "Nooit",
|
"shared_link_edit_expire_after_option_never": "Nooit",
|
||||||
"shared_link_edit_password": "Wachtwoord",
|
"shared_link_edit_password": "Wachtwoord",
|
||||||
"shared_link_edit_password_hint": "Voer wachtwoord voor de gedeelde link in",
|
"shared_link_edit_password_hint": "Voer het deel wachtwoord in",
|
||||||
"shared_link_edit_show_meta": "Toon metadata",
|
"shared_link_edit_show_meta": "Toon metadata",
|
||||||
"shared_link_edit_submit_button": "Link bijwerken",
|
"shared_link_edit_submit_button": "Update link",
|
||||||
"shared_link_empty": "Je hebt geen gedeelde links",
|
"shared_link_empty": "Je hebt geen gedeelde links",
|
||||||
"shared_link_error_server_url_fetch": "Kan de server url niet ophalen",
|
"shared_link_error_server_url_fetch": "Kan de server url niet ophalen",
|
||||||
"shared_link_expired": "Verlopen",
|
"shared_link_expired": "Verlopen",
|
||||||
@@ -453,19 +452,19 @@
|
|||||||
"theme_setting_three_stage_loading_title": "Laden in drie fasen inschakelen",
|
"theme_setting_three_stage_loading_title": "Laden in drie fasen inschakelen",
|
||||||
"translated_text_options": "Opties",
|
"translated_text_options": "Opties",
|
||||||
"trash_page_delete": "Verwijderen",
|
"trash_page_delete": "Verwijderen",
|
||||||
"trash_page_delete_all": "Verwijder alle",
|
"trash_page_delete_all": "Verwijder Alle",
|
||||||
"trash_page_empty_trash_btn": "Leeg prullenbak",
|
"trash_page_empty_trash_btn": "Leeg prullenbak",
|
||||||
"trash_page_empty_trash_dialog_content": "Wil je de prullenbak leegmaken? Deze items worden permanent verwijderd van Immich",
|
"trash_page_empty_trash_dialog_content": "Wil je je weggegooide assets leegmaken? Deze items worden permanent verwijderd van Immich",
|
||||||
"trash_page_empty_trash_dialog_ok": "Ok",
|
"trash_page_empty_trash_dialog_ok": "Ok",
|
||||||
"trash_page_info": "Verwijderde items worden permanent verwijderd na {} dagen",
|
"trash_page_info": "Verwijderde items worden permanent verwijderd na {} dagen",
|
||||||
"trash_page_no_assets": "Geen verwijderde assets",
|
"trash_page_no_assets": "Geen verwijderde assets",
|
||||||
"trash_page_restore": "Herstellen",
|
"trash_page_restore": "Herstellen",
|
||||||
"trash_page_restore_all": "Herstel alle",
|
"trash_page_restore_all": "Herstel Alle",
|
||||||
"trash_page_select_assets_btn": "Selecteer assets",
|
"trash_page_select_assets_btn": "Selecteer assets",
|
||||||
"trash_page_select_btn": "Selecteren",
|
"trash_page_select_btn": "Selecteren",
|
||||||
"trash_page_title": "Prullenbak ({})",
|
"trash_page_title": "Prullenbak ({})",
|
||||||
"upload_dialog_cancel": "Annuleren",
|
"upload_dialog_cancel": "Annuleren",
|
||||||
"upload_dialog_info": "Wil je een backup maken van de geselecteerde asset(s) op de server?",
|
"upload_dialog_info": "Wilt u een backup maken van de geselecteerde Asset(s) op de server?",
|
||||||
"upload_dialog_ok": "Uploaden",
|
"upload_dialog_ok": "Uploaden",
|
||||||
"upload_dialog_title": "Asset uploaden",
|
"upload_dialog_title": "Asset uploaden",
|
||||||
"version_announcement_overlay_ack": "Bevestig",
|
"version_announcement_overlay_ack": "Bevestig",
|
||||||
|
|||||||