1
0
forked from Cutlery/immich

Compare commits

..

3 Commits

Author SHA1 Message Date
Alex Tran da9e601ed5 segmentation and small fixes 2024-01-23 23:44:10 -06:00
Jason Rasmussen 88fd46dc9d feat: layout 2024-01-23 22:45:11 -05:00
Jason Rasmussen e320e31476 WIP 2024-01-23 22:08:25 -05:00
686 changed files with 18469 additions and 17214 deletions
+4 -3
View File
@@ -28,13 +28,14 @@ changelog:
labels: labels:
- documentation - documentation
- title: 🔨 Maintenance - title: 🔨 Build
labels: labels:
- deployment - deployment
- title: 🤖 Dependencies
labels:
- dependencies - dependencies
- renovate - renovate
- maintenance
- tech-debt
- title: Other changes - title: Other changes
labels: labels:
+3 -3
View File
@@ -20,7 +20,7 @@ jobs:
name: Build and sign Android name: Build and sign Android
# Skip when PR from a fork # Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
runs-on: macos-14 runs-on: macos-13
steps: steps:
- name: Determine ref - name: Determine ref
@@ -38,14 +38,14 @@ jobs:
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: "zulu" distribution: "zulu"
java-version: "11.0.21+9" java-version: "12.x"
cache: "gradle" cache: "gradle"
- 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.13.6"
cache: true cache: true
- name: Create the Keystore - name: Create the Keystore
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"
flutter-version: "3.16.9" flutter-version: "3.13.6"
- name: Install dependencies - name: Install dependencies
run: dart pub get run: dart pub get
+2 -2
View File
@@ -151,7 +151,7 @@ jobs:
run: npm ci run: npm ci
- name: Run npm install (server) - name: Run npm install (server)
run: npm ci && npm run build run: npm ci
working-directory: ./server working-directory: ./server
- name: Run e2e tests - name: Run e2e tests
@@ -204,7 +204,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"
flutter-version: "3.16.9" flutter-version: "3.13.6"
- name: Run tests - name: Run tests
working-directory: ./mobile working-directory: ./mobile
run: flutter test -j 1 run: flutter test -j 1
-3
View File
@@ -19,9 +19,6 @@ 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
prod: prod:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
+3 -7
View File
@@ -71,12 +71,10 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
## Features ## Features
| Features | Mobile | Web | | Features | Mobile | Web |
| :--------------------------------------------- | -------- | ----- | | -------------------------------------------- | ------ | --- |
| Upload and view videos and photos | Yes | Yes | | Upload and view videos and photos | Yes | Yes |
| Auto backup when the app is opened | Yes | N/A | | Auto backup when the app is opened | Yes | N/A |
| Prevent duplication of assets | Yes | Yes |
| Selective album(s) for backup | Yes | N/A | | Selective album(s) for backup | Yes | N/A |
| Download photos and videos to local device | Yes | Yes | | Download photos and videos to local device | Yes | Yes |
| Multi-user support | Yes | Yes | | Multi-user support | Yes | Yes |
@@ -91,7 +89,6 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| OAuth support | Yes | Yes | | OAuth support | Yes | Yes |
| API Keys | N/A | Yes | | API Keys | N/A | Yes |
| LivePhoto/MotionPhoto backup and playback | Yes | Yes | | LivePhoto/MotionPhoto backup and playback | Yes | Yes |
| Support 360 degree image display | No | Yes |
| User-defined storage structure | Yes | Yes | | User-defined storage structure | Yes | Yes |
| Public Sharing | No | Yes | | Public Sharing | No | Yes |
| Archive and Favorites | Yes | Yes | | Archive and Favorites | Yes | Yes |
@@ -113,15 +110,14 @@ If you feel like this is the right cause and the app is something you are seeing
### Donation ### Donation
- [Monthly donation](https://github.com/sponsors/immich-app) via GitHub Sponsors - [Monthly donation](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [One-time donation](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors - [One-time donation](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
- [Liberapay](https://liberapay.com/alex.tran1502/) - [Liberapay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
## Contributors ## Contributors
<a href="https://github.com/alextran1502/immich/graphs/contributors"> <a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/> <img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a> </a>
+2 -2
View File
@@ -106,8 +106,8 @@ Si creieu que aquesta és una causa justa i l'aplicació és alguna cosa que us
## Donació ## Donació
- [Donació mensual](https://github.com/sponsors/immich-app) a través de GitHub Sponsors - [Donació mensual](https://github.com/sponsors/alextran1502) a través de GitHub Sponsors
- [Donació única](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) a través de GitHub Sponsors - [Donació única](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) a través de GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+2 -2
View File
@@ -107,8 +107,8 @@ Si consideras que esta es una causa justa y la aplicación es algo que te gustar
## Donación ## Donación
- [Donación mensual](https://github.com/sponsors/immich-app) a través de GitHub Sponsors - [Donación mensual](https://github.com/sponsors/alextran1502) a través de GitHub Sponsors
- [Donación única](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) a través de GitHub Sponsors - [Donación única](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) a través de GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+2 -2
View File
@@ -108,8 +108,8 @@ Si vous estimez que c'est pour la bonne cause et que vous prévoyez d'utiliser l
## Donation ## Donation
- [Donation mensuelle](https://github.com/sponsors/immich-app) via GitHub Sponsors - [Donation mensuelle](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [Donation occasionnelle](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors - [Donation occasionnelle](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+2 -2
View File
@@ -109,8 +109,8 @@ Se pensi che Immich sia una buona causa e che l'app sia qualcosa che useresti ne
## Donazioni ## Donazioni
- [Donazione mensile](https://github.com/sponsors/immich-app) tramite GitHub Sponsors - [Donazione mensile](https://github.com/sponsors/alextran1502) tramite GitHub Sponsors
- [Donazione una tantum](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) tramite GitHub Sponsors - [Donazione una tantum](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) tramite GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+2 -2
View File
@@ -108,8 +108,8 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
## 寄付 ## 寄付
- GitHub スポンサー経由の[毎月の寄付](https://github.com/sponsors/immich-app) - GitHub スポンサー経由の[毎月の寄付](https://github.com/sponsors/alextran1502)
- GitHub スポンサー経由の[一回寄付](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) - GitHub スポンサー経由の[一回寄付](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+2 -2
View File
@@ -109,8 +109,8 @@ password: demo
### 후원 ### 후원
- GitHub 스폰서를 통한 [정기 후원](https://github.com/sponsors/immich-app) - GitHub 스폰서를 통한 [정기 후원](https://github.com/sponsors/alextran1502)
- GitHub 스폰서를 통한 [일시 후원](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) - GitHub 스폰서를 통한 [일시 후원](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+2 -2
View File
@@ -108,8 +108,8 @@ Als je denkt dat dit het juiste doel is en de app iets is dat je jezelf al heel
## Doneren ## Doneren
- [Maandelijkse donatie](https://github.com/sponsors/immich-app) via GitHub Sponsors - [Maandelijkse donatie](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [Eenmalige donatie](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors - [Eenmalige donatie](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+2 -2
View File
@@ -111,8 +111,8 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
### Пожертвование ### Пожертвование
- [Ежемесячное пожертвование](https://github.com/sponsors/immich-app) via GitHub Sponsors - [Ежемесячное пожертвование](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [Одноразовое пожертвование](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors - [Одноразовое пожертвование](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+2 -2
View File
@@ -105,8 +105,8 @@ Eğer bu size doğru bir amaç gibi geliyorsa ve uygulamanın uzun bir süre boy
## Bağış ## Bağış
- [Aylık bağış](https://github.com/sponsors/immich-app) via GitHub Sponsors - [Aylık bağış](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [Bir seferlik bağış](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors - [Bir seferlik bağış](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+2 -2
View File
@@ -115,8 +115,8 @@
## 捐赠 ## 捐赠
- 通过 GitHub Sponsors [按月捐赠](https://github.com/sponsors/immich-app) - 通过 GitHub Sponsors [按月捐赠](https://github.com/sponsors/alextran1502)
- 通过 Github Sponsors [单次捐赠](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) - 通过 Github Sponsors [单次捐赠](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- 比特币: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - 比特币: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+2 -2
View File
@@ -1,4 +1,4 @@
FROM ghcr.io/immich-app/base-server-dev:20240130@sha256:a11ac5c56f0ccce1f218954c07c43caadf489557252ba5b9ca1c5977aaa25999 as test FROM ghcr.io/immich-app/base-server-dev:20240118@sha256:92de0ebf1bc2ccc6b7d11023e31b010dc79dd7e16df04146820d2d2cc2e5bf9f as test
WORKDIR /usr/src/app/server WORKDIR /usr/src/app/server
COPY server/package.json server/package-lock.json ./ COPY server/package.json server/package-lock.json ./
@@ -10,7 +10,7 @@ COPY cli/package.json cli/package-lock.json ./
RUN npm ci RUN npm ci
COPY ./cli/ . COPY ./cli/ .
FROM ghcr.io/immich-app/base-server-prod:20240130@sha256:ce23a32154540b906df3c971766bcd991561c60331794e0ebb780947ac48113f FROM ghcr.io/immich-app/base-server-prod:20240118@sha256:5b78cefcc3c3dda2a48fd22c7b0fd6453cfc5ae61258ff5a082cc9563c4e3c25
VOLUME /usr/src/app/upload VOLUME /usr/src/app/upload
+4900 -2563
View File
File diff suppressed because it is too large Load Diff
+41 -9
View File
@@ -2,8 +2,7 @@
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.0.6", "version": "2.0.6",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "main": "dist/index.js",
"exports": "./dist/index.js",
"bin": { "bin": {
"immich": "./dist/src/index.js" "immich": "./dist/src/index.js"
}, },
@@ -14,45 +13,78 @@
], ],
"dependencies": { "dependencies": {
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"axios": "^1.6.7", "axios": "^1.6.2",
"byte-size": "^8.1.1", "byte-size": "^8.1.1",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
"commander": "^11.0.0", "commander": "^11.0.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"glob": "^10.3.1", "glob": "^10.3.1",
"graceful-fs": "^4.2.11",
"yaml": "^2.3.1" "yaml": "^2.3.1"
}, },
"devDependencies": { "devDependencies": {
"@testcontainers/postgresql": "^10.4.0", "@testcontainers/postgresql": "^10.4.0",
"@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/jest": "^29.5.2",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"@vitest/coverage-v8": "^1.2.2",
"eslint": "^8.43.0", "eslint": "^8.43.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.2.2",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-unicorn": "^50.0.0", "eslint-plugin-unicorn": "^50.0.0",
"immich": "file:../server", "immich": "file:../server",
"jest": "^29.5.0",
"jest-extended": "^4.0.0",
"jest-message-util": "^29.5.0",
"jest-mock-axios": "^4.7.2",
"jest-when": "^3.5.2",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tslib": "^2.5.3", "tslib": "^2.5.3",
"typescript": "^5.0.0", "typescript": "^5.0.0"
"vitest": "^1.2.1"
}, },
"scripts": { "scripts": {
"build": "tsc --project tsconfig.build.json", "build": "tsc --project tsconfig.build.json",
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0", "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0",
"lint:fix": "npm run lint -- --fix", "lint:fix": "npm run lint -- --fix",
"prepack": "npm run build", "prepack": "npm run build",
"test": "vitest", "test": "jest",
"test:cov": "vitest --coverage", "test:cov": "jest --coverage",
"format": "prettier --check .", "format": "prettier --check .",
"format:fix": "prettier --write .", "format:fix": "prettier --write .",
"check": "tsc --noEmit", "check": "tsc --noEmit",
"test:e2e": "vitest --config test/e2e/vitest.config.ts" "test:e2e": "jest --config test/e2e/jest-e2e.json --runInBand"
},
"jest": {
"clearMocks": true,
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": ".",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.ts$": "ts-jest"
},
"collectCoverageFrom": [
"<rootDir>/src/**/*.(t|j)s",
"!**/open-api/**"
],
"moduleNameMapper": {
"^@api(|/.*)$": "<rootDir>/src/api/$1",
"^@test(|/.*)$": "<rootDir>../server/test/$1",
"^@app/immich(|/.*)$": "<rootDir>../server/src/immich/$1",
"^@app/infra(|/.*)$": "<rootDir>../server/src/infra/$1",
"^@app/domain(|/.*)$": "<rootDir>../server/src/domain/$1"
},
"coverageDirectory": "./coverage",
"testEnvironment": "node"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -10,6 +10,7 @@ import {
SystemConfigApi, SystemConfigApi,
UserApi, UserApi,
} from '@immich/sdk'; } from '@immich/sdk';
import { ApiConfiguration } from '../cores/api-configuration';
import FormData from 'form-data'; import FormData from 'form-data';
export class ImmichApi { export class ImmichApi {
@@ -24,11 +25,10 @@ export class ImmichApi {
public systemConfigApi: SystemConfigApi; public systemConfigApi: SystemConfigApi;
private readonly config; private readonly config;
public readonly apiConfiguration: ApiConfiguration;
constructor( constructor(instanceUrl: string, apiKey: string) {
public instanceUrl: string, this.apiConfiguration = new ApiConfiguration(instanceUrl, apiKey);
public apiKey: string,
) {
this.config = new Configuration({ this.config = new Configuration({
basePath: instanceUrl, basePath: instanceUrl,
baseOptions: { baseOptions: {
@@ -49,9 +49,4 @@ export class ImmichApi {
this.keyApi = new APIKeyApi(this.config); this.keyApi = new APIKeyApi(this.config);
this.systemConfigApi = new SystemConfigApi(this.config); this.systemConfigApi = new SystemConfigApi(this.config);
} }
setApiKey(apiKey: string) {
this.apiKey = apiKey;
this.config.baseOptions.headers['x-api-key'] = apiKey;
}
} }
@@ -1,6 +1,9 @@
import { ServerVersionResponseDto, UserResponseDto } from '@immich/sdk'; import { ImmichApi } from '../api/client';
import { ImmichApi } from '../services/api.service';
import { SessionService } from '../services/session.service'; import { SessionService } from '../services/session.service';
import { LoginError } from '../cores/errors/login-error';
import { exit } from 'node:process';
import { ServerVersionResponseDto, UserResponseDto } from '@immich/sdk';
import { BaseOptionsDto } from 'src/cores/dto/base-options-dto';
export abstract class BaseCommand { export abstract class BaseCommand {
protected sessionService!: SessionService; protected sessionService!: SessionService;
@@ -8,7 +11,7 @@ export abstract class BaseCommand {
protected user!: UserResponseDto; protected user!: UserResponseDto;
protected serverVersion!: ServerVersionResponseDto; protected serverVersion!: ServerVersionResponseDto;
constructor(options: { config?: string }) { constructor(options: BaseOptionsDto) {
if (!options.config) { if (!options.config) {
throw new Error('Config directory is required'); throw new Error('Config directory is required');
} }
@@ -16,6 +19,15 @@ export abstract class BaseCommand {
} }
public async connect(): Promise<void> { public async connect(): Promise<void> {
this.immichApi = await this.sessionService.connect(); try {
this.immichApi = await this.sessionService.connect();
} catch (error) {
if (error instanceof LoginError) {
console.log(error.message);
exit(1);
} else {
throw error;
}
}
} }
} }
-7
View File
@@ -1,7 +0,0 @@
import { BaseCommand } from './base-command';
export class LoginCommand extends BaseCommand {
public async run(instanceUrl: string, apiKey: string): Promise<void> {
await this.sessionService.login(instanceUrl, apiKey);
}
}
+9
View File
@@ -0,0 +1,9 @@
import { BaseCommand } from '../../cli/base-command';
export class LoginKey extends BaseCommand {
public async run(instanceUrl: string, apiKey: string): Promise<void> {
console.log('Executing API key auth flow...');
await this.sessionService.keyLogin(instanceUrl, apiKey);
}
}
-8
View File
@@ -1,8 +0,0 @@
import { BaseCommand } from './base-command';
export class LogoutCommand extends BaseCommand {
public static readonly description = 'Logout and remove persisted credentials';
public async run(): Promise<void> {
await this.sessionService.logout();
}
}
+13
View File
@@ -0,0 +1,13 @@
import { BaseCommand } from '../cli/base-command';
export class Logout extends BaseCommand {
public static readonly description = 'Logout and remove persisted credentials';
public async run(): Promise<void> {
console.log('Executing logout flow...');
await this.sessionService.logout();
console.log('Successfully logged out');
}
}
-17
View File
@@ -1,17 +0,0 @@
import { BaseCommand } from './base-command';
export class ServerInfoCommand extends BaseCommand {
public async run() {
await this.connect();
const { data: versionInfo } = await this.immichApi.serverInfoApi.getServerVersion();
const { data: mediaTypes } = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
const { data: statistics } = await this.immichApi.assetApi.getAssetStatistics();
console.log(`Server Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`);
console.log(`Image Types: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`);
console.log(`Video Types: ${mediaTypes.video.map((extension) => extension.replace('.', ''))}`);
console.log(
`Statistics:\n Images: ${statistics.images}\n Videos: ${statistics.videos}\n Total: ${statistics.total}`,
);
}
}
+19
View File
@@ -0,0 +1,19 @@
import { BaseCommand } from '../cli/base-command';
export class ServerInfo extends BaseCommand {
public async run() {
await this.connect();
const { data: versionInfo } = await this.immichApi.serverInfoApi.getServerVersion();
console.log(`Server is running version ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`);
const { data: supportedmedia } = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
console.log(`Supported image types: ${supportedmedia.image.map((extension) => extension.replace('.', ''))}`);
console.log(`Supported video types: ${supportedmedia.video.map((extension) => extension.replace('.', ''))}`);
const { data: statistics } = await this.immichApi.assetApi.getAssetStatistics();
console.log(`Images: ${statistics.images}, Videos: ${statistics.videos}, Total: ${statistics.total}`);
}
}
@@ -1,112 +1,15 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; import { Asset } from '../cores/models/asset';
import byteSize from 'byte-size'; import { CrawlService } from '../services';
import { UploadOptionsDto } from '../cores/dto/upload-options-dto';
import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto';
import fs from 'node:fs';
import cliProgress from 'cli-progress'; import cliProgress from 'cli-progress';
import byteSize from 'byte-size';
import { BaseCommand } from '../cli/base-command';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import FormData from 'form-data'; import FormData from 'form-data';
import fs, { ReadStream, createReadStream } from 'node:fs';
import { CrawlService } from '../services/crawl.service';
import { BaseCommand } from './base-command';
import { basename } from 'node:path';
import { access, constants, stat, unlink } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import Os from 'os';
class Asset { export class Upload extends BaseCommand {
readonly path: string;
readonly deviceId!: string;
deviceAssetId?: string;
fileCreatedAt?: string;
fileModifiedAt?: string;
sidecarPath?: string;
fileSize!: number;
albumName?: string;
constructor(path: string) {
this.path = path;
}
async prepare() {
const stats = await stat(this.path);
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
this.fileCreatedAt = stats.mtime.toISOString();
this.fileModifiedAt = stats.mtime.toISOString();
this.fileSize = stats.size;
this.albumName = this.extractAlbumName();
}
async getUploadFormData(): Promise<FormData> {
if (!this.deviceAssetId) throw new Error('Device asset id not set');
if (!this.fileCreatedAt) throw new Error('File created at not set');
if (!this.fileModifiedAt) throw new Error('File modified at not set');
// TODO: doesn't xmp replace the file extension? Will need investigation
const sideCarPath = `${this.path}.xmp`;
let sidecarData: ReadStream | undefined = undefined;
try {
await access(sideCarPath, constants.R_OK);
sidecarData = createReadStream(sideCarPath);
} catch (error) {}
const data: any = {
assetData: createReadStream(this.path),
deviceAssetId: this.deviceAssetId,
deviceId: 'CLI',
fileCreatedAt: this.fileCreatedAt,
fileModifiedAt: this.fileModifiedAt,
isFavorite: String(false),
};
const formData = new FormData();
for (const prop in data) {
formData.append(prop, data[prop]);
}
if (sidecarData) {
formData.append('sidecarData', sidecarData);
}
return formData;
}
async delete(): Promise<void> {
return unlink(this.path);
}
public async hash(): Promise<string> {
const sha1 = (filePath: string) => {
const hash = createHash('sha1');
return new Promise<string>((resolve, reject) => {
const rs = createReadStream(filePath);
rs.on('error', reject);
rs.on('data', (chunk) => hash.update(chunk));
rs.on('end', () => resolve(hash.digest('hex')));
});
};
return await sha1(this.path);
}
private extractAlbumName(): string {
if (Os.platform() === 'win32') {
return this.path.split('\\').slice(-2)[0];
} else {
return this.path.split('/').slice(-2)[0];
}
}
}
export class UploadOptionsDto {
recursive? = false;
exclusionPatterns?: string[] = [];
dryRun? = false;
skipHash? = false;
delete? = false;
album? = false;
albumName? = '';
includeHidden? = false;
}
export class UploadCommand extends BaseCommand {
uploadLength!: number; uploadLength!: number;
public async run(paths: string[], options: UploadOptionsDto): Promise<void> { public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
@@ -115,29 +18,32 @@ export class UploadCommand extends BaseCommand {
const formatResponse = await this.immichApi.serverInfoApi.getSupportedMediaTypes(); const formatResponse = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
const crawlService = new CrawlService(formatResponse.data.image, formatResponse.data.video); const crawlService = new CrawlService(formatResponse.data.image, formatResponse.data.video);
const inputFiles: string[] = []; const crawlOptions = new CrawlOptionsDto();
crawlOptions.pathsToCrawl = paths;
crawlOptions.recursive = options.recursive;
crawlOptions.exclusionPatterns = options.exclusionPatterns;
crawlOptions.includeHidden = options.includeHidden;
const files: string[] = [];
for (const pathArgument of paths) { for (const pathArgument of paths) {
const fileStat = await fs.promises.lstat(pathArgument); const fileStat = await fs.promises.lstat(pathArgument);
if (fileStat.isFile()) { if (fileStat.isFile()) {
inputFiles.push(pathArgument); files.push(pathArgument);
} }
} }
const files: string[] = await crawlService.crawl({ const crawledFiles: string[] = await crawlService.crawl(crawlOptions);
pathsToCrawl: paths,
recursive: options.recursive,
exclusionPatterns: options.exclusionPatterns,
includeHidden: options.includeHidden,
});
files.push(...inputFiles); crawledFiles.push(...files);
if (files.length === 0) { if (crawledFiles.length === 0) {
console.log('No assets found, exiting'); console.log('No assets found, exiting');
return; return;
} }
const assetsToUpload = files.map((path) => new Asset(path)); const assetsToUpload = crawledFiles.map((path) => new Asset(path));
const uploadProgress = new cliProgress.SingleBar( const uploadProgress = new cliProgress.SingleBar(
{ {
@@ -198,7 +104,7 @@ export class UploadCommand extends BaseCommand {
if (!skipAsset) { if (!skipAsset) {
if (!options.dryRun) { if (!options.dryRun) {
if (!skipUpload) { if (!skipUpload) {
const formData = await asset.getUploadFormData(); const formData = asset.getUploadFormData();
const res = await this.uploadAsset(formData); const res = await this.uploadAsset(formData);
existingAssetId = res.data.id; existingAssetId = res.data.id;
uploadCounter++; uploadCounter++;
@@ -251,7 +157,7 @@ export class UploadCommand extends BaseCommand {
} else { } else {
console.log('Deleting assets that have been uploaded...'); console.log('Deleting assets that have been uploaded...');
const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic); const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic);
deletionProgress.start(files.length, 0); deletionProgress.start(crawledFiles.length, 0);
for (const asset of assetsToUpload) { for (const asset of assetsToUpload) {
if (!options.dryRun) { if (!options.dryRun) {
@@ -266,14 +172,14 @@ export class UploadCommand extends BaseCommand {
} }
private async uploadAsset(data: FormData): Promise<AxiosResponse> { private async uploadAsset(data: FormData): Promise<AxiosResponse> {
const url = this.immichApi.instanceUrl + '/asset/upload'; const url = this.immichApi.apiConfiguration.instanceUrl + '/asset/upload';
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
method: 'post', method: 'post',
maxRedirects: 0, maxRedirects: 0,
url, url,
headers: { headers: {
'x-api-key': this.immichApi.apiKey, 'x-api-key': this.immichApi.apiConfiguration.apiKey,
...data.getHeaders(), ...data.getHeaders(),
}, },
maxContentLength: Infinity, maxContentLength: Infinity,
+5 -5
View File
@@ -1,12 +1,12 @@
import pkg from '../package.json'; import pkg from '../package.json';
export interface ICliVersion { export interface ICLIVersion {
major: number; major: number;
minor: number; minor: number;
patch: number; patch: number;
} }
export class CliVersion implements ICliVersion { export class CLIVersion implements ICLIVersion {
constructor( constructor(
public readonly major: number, public readonly major: number,
public readonly minor: number, public readonly minor: number,
@@ -22,16 +22,16 @@ export class CliVersion implements ICliVersion {
return { major, minor, patch }; return { major, minor, patch };
} }
static fromString(version: string): CliVersion { static fromString(version: string): CLIVersion {
const regex = /(?:v)?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i; const regex = /(?:v)?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
const matchResult = version.match(regex); const matchResult = version.match(regex);
if (matchResult) { if (matchResult) {
const [, major, minor, patch] = matchResult.map(Number); const [, major, minor, patch] = matchResult.map(Number);
return new CliVersion(major, minor, patch); return new CLIVersion(major, minor, patch);
} else { } else {
throw new Error(`Invalid version format: ${version}`); throw new Error(`Invalid version format: ${version}`);
} }
} }
} }
export const cliVersion = CliVersion.fromString(pkg.version); export const cliVersion = CLIVersion.fromString(pkg.version);
+9
View File
@@ -0,0 +1,9 @@
export class ApiConfiguration {
public readonly instanceUrl!: string;
public readonly apiKey!: string;
constructor(instanceUrl: string, apiKey: string) {
this.instanceUrl = instanceUrl;
this.apiKey = apiKey;
}
}
+3
View File
@@ -0,0 +1,3 @@
export class BaseOptionsDto {
config?: string;
}
+6
View File
@@ -0,0 +1,6 @@
export class CrawlOptionsDto {
pathsToCrawl!: string[];
recursive? = false;
includeHidden? = false;
exclusionPatterns?: string[];
}
+10
View File
@@ -0,0 +1,10 @@
export class UploadOptionsDto {
recursive? = false;
exclusionPatterns?: string[] = [];
dryRun? = false;
skipHash? = false;
delete? = false;
album? = false;
albumName? = '';
includeHidden? = false;
}
+9
View File
@@ -0,0 +1,9 @@
export class LoginError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
+1
View File
@@ -0,0 +1 @@
export * from './models';
+91
View File
@@ -0,0 +1,91 @@
import crypto from 'crypto';
import FormData from 'form-data';
import * as fs from 'graceful-fs';
import { createReadStream } from 'node:fs';
import { basename } from 'node:path';
import Os from 'os';
export class Asset {
readonly path: string;
readonly deviceId!: string;
deviceAssetId?: string;
fileCreatedAt?: string;
fileModifiedAt?: string;
sidecarPath?: string;
fileSize!: number;
albumName?: string;
constructor(path: string) {
this.path = path;
}
async prepare() {
const stats = await fs.promises.stat(this.path);
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
this.fileCreatedAt = stats.mtime.toISOString();
this.fileModifiedAt = stats.mtime.toISOString();
this.fileSize = stats.size;
this.albumName = this.extractAlbumName();
}
getUploadFormData(): FormData {
if (!this.deviceAssetId) throw new Error('Device asset id not set');
if (!this.fileCreatedAt) throw new Error('File created at not set');
if (!this.fileModifiedAt) throw new Error('File modified at not set');
// TODO: doesn't xmp replace the file extension? Will need investigation
const sideCarPath = `${this.path}.xmp`;
let sidecarData: fs.ReadStream | undefined = undefined;
try {
fs.accessSync(sideCarPath, fs.constants.R_OK);
sidecarData = createReadStream(sideCarPath);
} catch (error) {}
const data: any = {
assetData: createReadStream(this.path),
deviceAssetId: this.deviceAssetId,
deviceId: 'CLI',
fileCreatedAt: this.fileCreatedAt,
fileModifiedAt: this.fileModifiedAt,
isFavorite: String(false),
};
const formData = new FormData();
for (const prop in data) {
formData.append(prop, data[prop]);
}
if (sidecarData) {
formData.append('sidecarData', sidecarData);
}
return formData;
}
async delete(): Promise<void> {
return fs.promises.unlink(this.path);
}
public async hash(): Promise<string> {
const sha1 = (filePath: string) => {
const hash = crypto.createHash('sha1');
return new Promise<string>((resolve, reject) => {
const rs = fs.createReadStream(filePath);
rs.on('error', reject);
rs.on('data', (chunk) => hash.update(chunk));
rs.on('end', () => resolve(hash.digest('hex')));
});
};
return await sha1(this.path);
}
private extractAlbumName(): string {
if (Os.platform() === 'win32') {
return this.path.split('\\').slice(-2)[0];
} else {
return this.path.split('/').slice(-2)[0];
}
}
}
+1
View File
@@ -0,0 +1 @@
export * from './asset';
+12 -10
View File
@@ -1,12 +1,14 @@
#! /usr/bin/env node #! /usr/bin/env node
import { Command, Option } from 'commander';
import { Option, Command } from 'commander';
import { Upload } from './commands/upload';
import { ServerInfo } from './commands/server-info';
import { LoginKey } from './commands/login/key';
import { Logout } from './commands/logout';
import { version } from '../package.json';
import path from 'node:path'; import path from 'node:path';
import os from 'os'; import os from 'os';
import { version } from '../package.json';
import { LoginCommand } from './commands/login';
import { LogoutCommand } from './commands/logout.command';
import { ServerInfoCommand } from './commands/server-info.command';
import { UploadCommand } from './commands/upload.command';
const userHomeDir = os.homedir(); const userHomeDir = os.homedir();
const configDir = path.join(userHomeDir, '.config/immich/'); const configDir = path.join(userHomeDir, '.config/immich/');
@@ -44,14 +46,14 @@ program
.argument('[paths...]', 'One or more paths to assets to be uploaded') .argument('[paths...]', 'One or more paths to assets to be uploaded')
.action(async (paths, options) => { .action(async (paths, options) => {
options.exclusionPatterns = options.ignore; options.exclusionPatterns = options.ignore;
await new UploadCommand(program.opts()).run(paths, options); await new Upload(program.opts()).run(paths, options);
}); });
program program
.command('server-info') .command('server-info')
.description('Display server information') .description('Display server information')
.action(async () => { .action(async () => {
await new ServerInfoCommand(program.opts()).run(); await new ServerInfo(program.opts()).run();
}); });
program program
@@ -60,14 +62,14 @@ program
.argument('[instanceUrl]') .argument('[instanceUrl]')
.argument('[apiKey]') .argument('[apiKey]')
.action(async (paths, options) => { .action(async (paths, options) => {
await new LoginCommand(program.opts()).run(paths, options); await new LoginKey(program.opts()).run(paths, options);
}); });
program program
.command('logout') .command('logout')
.description('Remove stored credentials') .description('Remove stored credentials')
.action(async () => { .action(async () => {
await new LogoutCommand(program.opts()).run(); await new Logout(program.opts()).run();
}); });
program.parse(process.argv); program.parse(process.argv);
+3 -2
View File
@@ -1,9 +1,10 @@
import mockfs from 'mock-fs'; import mockfs from 'mock-fs';
import { CrawlService, CrawlOptions } from './crawl.service'; import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto';
import { CrawlService } from '.';
interface Test { interface Test {
test: string; test: string;
options: CrawlOptions; options: CrawlOptionsDto;
files: Record<string, boolean>; files: Record<string, boolean>;
} }
+4 -11
View File
@@ -1,13 +1,7 @@
import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto';
import { glob } from 'glob'; import { glob } from 'glob';
import * as fs from 'fs'; import * as fs from 'fs';
export class CrawlOptions {
pathsToCrawl!: string[];
recursive? = false;
includeHidden? = false;
exclusionPatterns?: string[];
}
export class CrawlService { export class CrawlService {
private readonly extensions!: string[]; private readonly extensions!: string[];
@@ -15,9 +9,8 @@ export class CrawlService {
this.extensions = image.concat(video).map((extension) => extension.replace('.', '')); this.extensions = image.concat(video).map((extension) => extension.replace('.', ''));
} }
async crawl(options: CrawlOptions): Promise<string[]> { async crawl(crawlOptions: CrawlOptionsDto): Promise<string[]> {
const { recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options; const { pathsToCrawl, exclusionPatterns, includeHidden } = crawlOptions;
if (!pathsToCrawl) { if (!pathsToCrawl) {
return Promise.resolve([]); return Promise.resolve([]);
} }
@@ -51,7 +44,7 @@ export class CrawlService {
searchPattern = '{' + patterns.join(',') + '}'; searchPattern = '{' + patterns.join(',') + '}';
} }
if (recursive) { if (crawlOptions.recursive) {
searchPattern = searchPattern + '/**/'; searchPattern = searchPattern + '/**/';
} }
+1
View File
@@ -0,0 +1 @@
export * from './crawl.service';
+25 -20
View File
@@ -1,6 +1,7 @@
import { SessionService } from './session.service'; import { SessionService } from './session.service';
import fs from 'node:fs'; import fs from 'node:fs';
import yaml from 'yaml'; import yaml from 'yaml';
import { LoginError } from '../cores/errors/login-error';
import { import {
TEST_AUTH_FILE, TEST_AUTH_FILE,
TEST_CONFIG_DIR, TEST_CONFIG_DIR,
@@ -12,21 +13,28 @@ import {
spyOnConsole, spyOnConsole,
} from '../../test/cli-test-utils'; } from '../../test/cli-test-utils';
const mockPingServer = vi.fn(() => Promise.resolve({ data: { res: 'pong' } })); const mockPingServer = jest.fn(() => Promise.resolve({ data: { res: 'pong' } }));
const mockUserInfo = vi.fn(() => Promise.resolve({ data: { email: 'admin@example.com' } })); const mockUserInfo = jest.fn(() => Promise.resolve({ data: { email: 'admin@example.com' } }));
vi.mock('@immich/sdk', async () => ({ jest.mock('@immich/sdk', () => {
...(await vi.importActual('@immich/sdk')), return {
UserApi: vi.fn().mockImplementation(() => { ...jest.requireActual('@immich/sdk'),
return { getMyUserInfo: mockUserInfo }; UserApi: jest.fn().mockImplementation(() => {
}), return { getMyUserInfo: mockUserInfo };
ServerInfoApi: vi.fn().mockImplementation(() => { }),
return { pingServer: mockPingServer }; ServerInfoApi: jest.fn().mockImplementation(() => {
}), return { pingServer: mockPingServer };
})); }),
};
});
describe('SessionService', () => { describe('SessionService', () => {
let sessionService: SessionService; let sessionService: SessionService;
let consoleSpy: jest.SpyInstance;
beforeAll(() => {
consoleSpy = spyOnConsole();
});
beforeEach(() => { beforeEach(() => {
deleteAuthFile(); deleteAuthFile();
@@ -62,6 +70,7 @@ describe('SessionService', () => {
}), }),
); );
await sessionService.connect().catch((error) => { await sessionService.connect().catch((error) => {
expect(error).toBeInstanceOf(LoginError);
expect(error.message).toEqual(`Instance URL missing in auth config file ${TEST_AUTH_FILE}`); expect(error.message).toEqual(`Instance URL missing in auth config file ${TEST_AUTH_FILE}`);
}); });
}); });
@@ -73,11 +82,13 @@ describe('SessionService', () => {
}), }),
); );
await expect(sessionService.connect()).rejects.toThrow(`API key missing in auth config file ${TEST_AUTH_FILE}`); await expect(sessionService.connect()).rejects.toThrow(
new LoginError(`API key missing in auth config file ${TEST_AUTH_FILE}`),
);
}); });
it('should create auth file when logged in', async () => { it('should create auth file when logged in', async () => {
await sessionService.login(TEST_IMMICH_INSTANCE_URL, TEST_IMMICH_API_KEY); await sessionService.keyLogin(TEST_IMMICH_INSTANCE_URL, TEST_IMMICH_API_KEY);
const data: string = await readTestAuthFile(); const data: string = await readTestAuthFile();
const authConfig = yaml.parse(data); const authConfig = yaml.parse(data);
@@ -86,8 +97,6 @@ describe('SessionService', () => {
}); });
it('should delete auth file when logging out', async () => { it('should delete auth file when logging out', async () => {
const consoleSpy = spyOnConsole();
await createTestAuthFile( await createTestAuthFile(
JSON.stringify({ JSON.stringify({
apiKey: TEST_IMMICH_API_KEY, apiKey: TEST_IMMICH_API_KEY,
@@ -100,10 +109,6 @@ describe('SessionService', () => {
expect(error.message).toContain('ENOENT'); expect(error.message).toContain('ENOENT');
}); });
expect(consoleSpy.mock.calls).toEqual([ expect(consoleSpy.mock.calls).toEqual([[`Removed auth file ${TEST_AUTH_FILE}`]]);
['Logging out...'],
[`Removed auth file ${TEST_AUTH_FILE}`],
['Successfully logged out'],
]);
}); });
}); });
+30 -42
View File
@@ -1,40 +1,31 @@
import { existsSync } from 'fs'; import fs from 'node:fs';
import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
import path from 'node:path';
import yaml from 'yaml'; import yaml from 'yaml';
import { ImmichApi } from './api.service'; import path from 'node:path';
import { ImmichApi } from '../api/client';
class LoginError extends Error { import { LoginError } from '../cores/errors/login-error';
constructor(message: string) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
export class SessionService { export class SessionService {
readonly configDir!: string; readonly configDir!: string;
readonly authPath!: string; readonly authPath!: string;
private api!: ImmichApi;
constructor(configDir: string) { constructor(configDir: string) {
this.configDir = configDir; this.configDir = configDir;
this.authPath = path.join(configDir, '/auth.yml'); this.authPath = path.join(configDir, '/auth.yml');
} }
async connect(): Promise<ImmichApi> { public async connect(): Promise<ImmichApi> {
let instanceUrl = process.env.IMMICH_INSTANCE_URL; let instanceUrl = process.env.IMMICH_INSTANCE_URL;
let apiKey = process.env.IMMICH_API_KEY; let apiKey = process.env.IMMICH_API_KEY;
if (!instanceUrl || !apiKey) { if (!instanceUrl || !apiKey) {
await access(this.authPath, constants.F_OK).catch((error) => { await fs.promises.access(this.authPath, fs.constants.F_OK).catch((error) => {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
throw new LoginError('No auth file exist. Please login first'); throw new LoginError('No auth file exist. Please login first');
} }
}); });
const data: string = await readFile(this.authPath, 'utf8'); const data: string = await fs.promises.readFile(this.authPath, 'utf8');
const parsedConfig = yaml.parse(data); const parsedConfig = yaml.parse(data);
instanceUrl = parsedConfig.instanceUrl; instanceUrl = parsedConfig.instanceUrl;
@@ -49,54 +40,51 @@ export class SessionService {
} }
} }
const api = new ImmichApi(instanceUrl, apiKey); this.api = new ImmichApi(instanceUrl, apiKey);
const { data: pingResponse } = await api.serverInfoApi.pingServer().catch((error) => { await this.ping();
throw new Error(`Failed to connect to server ${api.instanceUrl}: ${error.message}`);
});
if (pingResponse.res !== 'pong') { return this.api;
throw new Error(`Could not parse response. Is Immich listening on ${api.instanceUrl}?`);
}
return api;
} }
async login(instanceUrl: string, apiKey: string): Promise<ImmichApi> { public async keyLogin(instanceUrl: string, apiKey: string): Promise<ImmichApi> {
console.log('Logging in...'); this.api = new ImmichApi(instanceUrl, apiKey);
const api = new ImmichApi(instanceUrl, apiKey);
// Check if server and api key are valid // Check if server and api key are valid
const { data: userInfo } = await api.userApi.getMyUserInfo().catch((error) => { const { data: userInfo } = await this.api.userApi.getMyUserInfo().catch((error) => {
throw new LoginError(`Failed to connect to server ${instanceUrl}: ${error.message}`); throw new LoginError(`Failed to connect to server ${instanceUrl}: ${error.message}`);
}); });
console.log(`Logged in as ${userInfo.email}`); console.log(`Logged in as ${userInfo.email}`);
if (!existsSync(this.configDir)) { if (!fs.existsSync(this.configDir)) {
// Create config folder if it doesn't exist // Create config folder if it doesn't exist
const created = await mkdir(this.configDir, { recursive: true }); const created = await fs.promises.mkdir(this.configDir, { recursive: true });
if (!created) { if (!created) {
throw new Error(`Failed to create config folder ${this.configDir}`); throw new Error(`Failed to create config folder ${this.configDir}`);
} }
} }
await writeFile(this.authPath, yaml.stringify({ instanceUrl, apiKey })); fs.writeFileSync(this.authPath, yaml.stringify({ instanceUrl, apiKey }));
console.log('Wrote auth info to ' + this.authPath); console.log('Wrote auth info to ' + this.authPath);
return this.api;
return api;
} }
async logout(): Promise<void> { public async logout(): Promise<void> {
console.log('Logging out...'); if (fs.existsSync(this.authPath)) {
fs.unlinkSync(this.authPath);
if (existsSync(this.authPath)) {
await unlink(this.authPath);
console.log('Removed auth file ' + this.authPath); console.log('Removed auth file ' + this.authPath);
} }
}
console.log('Successfully logged out'); private async ping(): Promise<void> {
const { data: pingResponse } = await this.api.serverInfoApi.pingServer().catch((error) => {
throw new Error(`Failed to connect to server ${this.api.apiConfiguration.instanceUrl}: ${error.message}`);
});
if (pingResponse.res !== 'pong') {
throw new Error(`Could not parse response. Is Immich listening on ${this.api.apiConfiguration.instanceUrl}?`);
}
} }
} }
+3 -21
View File
@@ -1,33 +1,15 @@
import { BaseOptionsDto } from 'src/cores/dto/base-options-dto';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { ImmichApi } from '../src/services/api.service';
export const TEST_CONFIG_DIR = '/tmp/immich/'; export const TEST_CONFIG_DIR = '/tmp/immich/';
export const TEST_AUTH_FILE = path.join(TEST_CONFIG_DIR, 'auth.yml'); export const TEST_AUTH_FILE = path.join(TEST_CONFIG_DIR, 'auth.yml');
export const TEST_IMMICH_INSTANCE_URL = 'https://test/api'; export const TEST_IMMICH_INSTANCE_URL = 'https://test/api';
export const TEST_IMMICH_API_KEY = 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg'; export const TEST_IMMICH_API_KEY = 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg';
export const CLI_BASE_OPTIONS = { config: TEST_CONFIG_DIR }; export const CLI_BASE_OPTIONS: BaseOptionsDto = { config: TEST_CONFIG_DIR };
export const setup = async () => { export const spyOnConsole = () => jest.spyOn(console, 'log').mockImplementation();
const api = new ImmichApi(process.env.IMMICH_INSTANCE_URL as string, '');
await api.authenticationApi.signUpAdmin({
signUpDto: { email: 'cli@immich.app', password: 'password', name: 'Administrator' },
});
const { data: admin } = await api.authenticationApi.login({
loginCredentialDto: { email: 'cli@immich.app', password: 'password' },
});
const { data: apiKey } = await api.keyApi.createApiKey(
{ aPIKeyCreateDto: { name: 'CLI Test' } },
{ headers: { Authorization: `Bearer ${admin.accessToken}` } },
);
api.setApiKey(apiKey.secret);
return api;
};
export const spyOnConsole = () => vi.spyOn(console, 'log').mockImplementation(() => {});
export const createTestAuthFile = async (contents: string) => { export const createTestAuthFile = async (contents: string) => {
if (!fs.existsSync(TEST_CONFIG_DIR)) { if (!fs.existsSync(TEST_CONFIG_DIR)) {
+24
View File
@@ -0,0 +1,24 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"modulePaths": ["<rootDir>"],
"rootDir": "../..",
"globalSetup": "<rootDir>/test/e2e/setup.ts",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"testTimeout": 6000000,
"transform": {
"^.+\\.ts$": "ts-jest"
},
"collectCoverageFrom": [
"<rootDir>/src/**/*.(t|j)s",
"!<rootDir>/src/**/*.spec.(t|s)s",
"!<rootDir>/src/infra/migrations/**"
],
"coverageDirectory": "./coverage",
"moduleNameMapper": {
"^@test(|/.*)$": "<rootDir>../server/test/$1",
"^@app/immich(|/.*)$": "<rootDir>../server/src/immich/$1",
"^@app/infra(|/.*)$": "<rootDir>../server/src/infra/$1",
"^@app/domain(|/.*)$": "<rootDir>/../server/src/domain/$1"
}
}
+18 -12
View File
@@ -1,15 +1,20 @@
import { restoreTempFolder, testApp } from '@test-utils'; import { APIKeyCreateResponseDto } from '@app/domain';
import { CLI_BASE_OPTIONS, setup, spyOnConsole } from 'test/cli-test-utils'; import { api } from '@test/../e2e/api/client';
import { LoginCommand } from '../../src/commands/login'; import { restoreTempFolder, testApp } from '@test/../e2e/jobs/utils';
import { LoginResponseDto } from '@immich/sdk';
import { LoginKey } from 'src/commands/login/key';
import { LoginError } from 'src/cores/errors/login-error';
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
describe(`login-key (e2e)`, () => { describe(`login-key (e2e)`, () => {
let apiKey: string; let server: any;
let admin: LoginResponseDto;
let apiKey: APIKeyCreateResponseDto;
let instanceUrl: string; let instanceUrl: string;
spyOnConsole(); spyOnConsole();
beforeAll(async () => { beforeAll(async () => {
await testApp.create(); server = (await testApp.create()).getHttpServer();
if (!process.env.IMMICH_INSTANCE_URL) { if (!process.env.IMMICH_INSTANCE_URL) {
throw new Error('IMMICH_INSTANCE_URL environment variable not set'); throw new Error('IMMICH_INSTANCE_URL environment variable not set');
} else { } else {
@@ -25,18 +30,19 @@ describe(`login-key (e2e)`, () => {
beforeEach(async () => { beforeEach(async () => {
await testApp.reset(); await testApp.reset();
await restoreTempFolder(); await restoreTempFolder();
await api.authApi.adminSignUp(server);
const api = await setup(); admin = await api.authApi.adminLogin(server);
apiKey = api.apiKey; apiKey = await api.apiKeyApi.createApiKey(server, admin.accessToken);
process.env.IMMICH_API_KEY = apiKey.secret;
}); });
it('should error when providing an invalid API key', async () => { it('should error when providing an invalid API key', async () => {
await expect(new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, 'invalid')).rejects.toThrow( await expect(async () => await new LoginKey(CLI_BASE_OPTIONS).run(instanceUrl, 'invalid')).rejects.toThrow(
`Failed to connect to server ${instanceUrl}: Request failed with status code 401`, new LoginError(`Failed to connect to server ${instanceUrl}: Request failed with status code 401`),
); );
}); });
it('should log in when providing the correct API key', async () => { it('should log in when providing the correct API key', async () => {
await new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, apiKey); await new LoginKey(CLI_BASE_OPTIONS).run(instanceUrl, apiKey.secret);
}); });
}); });
+19 -11
View File
@@ -1,12 +1,18 @@
import { restoreTempFolder, testApp } from '@test-utils'; import { APIKeyCreateResponseDto } from '@app/domain';
import { CLI_BASE_OPTIONS, setup, spyOnConsole } from 'test/cli-test-utils'; import { api } from '@test/../e2e/api/client';
import { ServerInfoCommand } from '../../src/commands/server-info.command'; import { restoreTempFolder, testApp } from '@test/../e2e/jobs/utils';
import { LoginResponseDto } from '@immich/sdk';
import { ServerInfo } from 'src/commands/server-info';
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
describe(`server-info (e2e)`, () => { describe(`server-info (e2e)`, () => {
let server: any;
let admin: LoginResponseDto;
let apiKey: APIKeyCreateResponseDto;
const consoleSpy = spyOnConsole(); const consoleSpy = spyOnConsole();
beforeAll(async () => { beforeAll(async () => {
await testApp.create(); server = (await testApp.create()).getHttpServer();
}); });
afterAll(async () => { afterAll(async () => {
@@ -17,18 +23,20 @@ describe(`server-info (e2e)`, () => {
beforeEach(async () => { beforeEach(async () => {
await testApp.reset(); await testApp.reset();
await restoreTempFolder(); await restoreTempFolder();
const api = await setup(); await api.authApi.adminSignUp(server);
process.env.IMMICH_API_KEY = api.apiKey; admin = await api.authApi.adminLogin(server);
apiKey = await api.apiKeyApi.createApiKey(server, admin.accessToken);
process.env.IMMICH_API_KEY = apiKey.secret;
}); });
it('should show server version', async () => { it('should show server version', async () => {
await new ServerInfoCommand(CLI_BASE_OPTIONS).run(); await new ServerInfo(CLI_BASE_OPTIONS).run();
expect(consoleSpy.mock.calls).toEqual([ expect(consoleSpy.mock.calls).toEqual([
[expect.stringMatching(new RegExp('Server Version: \\d+.\\d+.\\d+'))], [expect.stringMatching(new RegExp('Server is running version \\d+.\\d+.\\d+'))],
[expect.stringMatching('Image Types: .*')], [expect.stringMatching('Supported image types: .*')],
[expect.stringMatching('Video Types: .*')], [expect.stringMatching('Supported video types: .*')],
['Statistics:\n Images: 0\n Videos: 0\n Total: 0'], ['Images: 0, Videos: 0, Total: 0'],
]); ]);
}); });
}); });
+26 -21
View File
@@ -1,15 +1,18 @@
import { IMMICH_TEST_ASSET_PATH, restoreTempFolder, testApp } from '@test-utils'; import { APIKeyCreateResponseDto } from '@app/domain';
import { CLI_BASE_OPTIONS, setup, spyOnConsole } from 'test/cli-test-utils'; import { api } from '@test/../e2e/api/client';
import { UploadCommand } from '../../src/commands/upload.command'; import { IMMICH_TEST_ASSET_PATH, restoreTempFolder, testApp } from '@test/../e2e/jobs/utils';
import { ImmichApi } from '../../src/services/api.service'; import { LoginResponseDto } from '@immich/sdk';
import { Upload } from 'src/commands/upload';
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
describe(`upload (e2e)`, () => { describe(`upload (e2e)`, () => {
let api: ImmichApi; let server: any;
let admin: LoginResponseDto;
let apiKey: APIKeyCreateResponseDto;
spyOnConsole(); spyOnConsole();
beforeAll(async () => { beforeAll(async () => {
await testApp.create(); server = (await testApp.create()).getHttpServer();
}); });
afterAll(async () => { afterAll(async () => {
@@ -20,58 +23,60 @@ describe(`upload (e2e)`, () => {
beforeEach(async () => { beforeEach(async () => {
await testApp.reset(); await testApp.reset();
await restoreTempFolder(); await restoreTempFolder();
api = await setup(); await api.authApi.adminSignUp(server);
process.env.IMMICH_API_KEY = api.apiKey; admin = await api.authApi.adminLogin(server);
apiKey = await api.apiKeyApi.createApiKey(server, admin.accessToken);
process.env.IMMICH_API_KEY = apiKey.secret;
}); });
it('should upload a folder recursively', async () => { it('should upload a folder recursively', async () => {
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true }); await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true });
const { data: assets } = await api.assetApi.getAllAssets({}, { headers: { 'x-api-key': api.apiKey } }); const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(assets.length).toBeGreaterThan(4); expect(assets.length).toBeGreaterThan(4);
}); });
it('should not create a new album', async () => { it('should not create a new album', async () => {
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true }); await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true });
const { data: albums } = await api.albumApi.getAllAlbums({}, { headers: { 'x-api-key': api.apiKey } }); const albums = await api.albumApi.getAllAlbums(server, admin.accessToken);
expect(albums.length).toEqual(0); expect(albums.length).toEqual(0);
}); });
it('should create album from folder name', async () => { it('should create album from folder name', async () => {
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
recursive: true, recursive: true,
album: true, album: true,
}); });
const { data: albums } = await api.albumApi.getAllAlbums({}, { headers: { 'x-api-key': api.apiKey } }); const albums = await api.albumApi.getAllAlbums(server, admin.accessToken);
expect(albums.length).toEqual(1); expect(albums.length).toEqual(1);
const natureAlbum = albums[0]; const natureAlbum = albums[0];
expect(natureAlbum.albumName).toEqual('nature'); expect(natureAlbum.albumName).toEqual('nature');
}); });
it('should add existing assets to album', async () => { it('should add existing assets to album', async () => {
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
recursive: true, recursive: true,
}); });
// upload again, but this time add to album // Upload again, but this time add to album
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
recursive: true, recursive: true,
album: true, album: true,
}); });
const { data: albums } = await api.albumApi.getAllAlbums({}, { headers: { 'x-api-key': api.apiKey } }); const albums = await api.albumApi.getAllAlbums(server, admin.accessToken);
expect(albums.length).toEqual(1); expect(albums.length).toEqual(1);
const natureAlbum = albums[0]; const natureAlbum = albums[0];
expect(natureAlbum.albumName).toEqual('nature'); expect(natureAlbum.albumName).toEqual('nature');
}); });
it('should upload to the specified album name', async () => { it('should upload to the specified album name', async () => {
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
recursive: true, recursive: true,
albumName: 'testAlbum', albumName: 'testAlbum',
}); });
const { data: albums } = await api.albumApi.getAllAlbums({}, { headers: { 'x-api-key': api.apiKey } }); const albums = await api.albumApi.getAllAlbums(server, admin.accessToken);
expect(albums.length).toEqual(1); expect(albums.length).toEqual(1);
const testAlbum = albums[0]; const testAlbum = albums[0];
expect(testAlbum.albumName).toEqual('testAlbum'); expect(testAlbum.albumName).toEqual('testAlbum');
-22
View File
@@ -1,22 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
resolve: {
alias: {
'@test-utils': new URL('../../../server/dist/test-utils/utils.js', import.meta.url).pathname,
},
},
test: {
include: ['**/*.e2e-spec.ts'],
globals: true,
globalSetup: 'test/e2e/setup.ts',
pool: 'forks',
poolOptions: {
forks: {
maxForks: 1,
minForks: 1,
},
},
testTimeout: 10000,
},
});
+2 -5
View File
@@ -1,7 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "esnext", "module": "commonjs",
"moduleResolution": "bundler",
"strict": true, "strict": true,
"declaration": true, "declaration": true,
"removeComments": true, "removeComments": true,
@@ -20,15 +19,13 @@
"paths": { "paths": {
"@test": ["../server/test"], "@test": ["../server/test"],
"@test/*": ["../server/test/*"], "@test/*": ["../server/test/*"],
"@test-utils": ["../server/src/test-utils/utils"],
"@app/immich": ["../server/src/immich"], "@app/immich": ["../server/src/immich"],
"@app/immich/*": ["../server/src/immich/*"], "@app/immich/*": ["../server/src/immich/*"],
"@app/infra": ["../server/src/infra"], "@app/infra": ["../server/src/infra"],
"@app/infra/*": ["../server/src/infra/*"], "@app/infra/*": ["../server/src/infra/*"],
"@app/domain": ["../server/src/domain"], "@app/domain": ["../server/src/domain"],
"@app/domain/*": ["../server/src/domain/*"] "@app/domain/*": ["../server/src/domain/*"]
}, }
"types": ["vitest/globals"]
}, },
"exclude": ["dist", "node_modules", "upload"] "exclude": ["dist", "node_modules", "upload"]
} }
-7
View File
@@ -1,7 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
},
});
+1 -1
View File
@@ -99,7 +99,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:afb290a0a0d0b2bd7537b62ebff1eb84d045c757c1c31ca2ca48c79536c0de82 image: redis:6.2-alpine@sha256:c5a607fb6e1bb15d32bbcf14db22787d19e428d59e31a5da67511b49bb0f1ccc
database: database:
container_name: immich_postgres container_name: immich_postgres
+1 -1
View File
@@ -56,7 +56,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:afb290a0a0d0b2bd7537b62ebff1eb84d045c757c1c31ca2ca48c79536c0de82 image: redis:6.2-alpine@sha256:c5a607fb6e1bb15d32bbcf14db22787d19e428d59e31a5da67511b49bb0f1ccc
restart: always restart: always
database: database:
+1 -1
View File
@@ -60,7 +60,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:afb290a0a0d0b2bd7537b62ebff1eb84d045c757c1c31ca2ca48c79536c0de82 image: redis:6.2-alpine@sha256:c5a607fb6e1bb15d32bbcf14db22787d19e428d59e31a5da67511b49bb0f1ccc
restart: always restart: always
database: database:
+1 -19
View File
@@ -86,10 +86,6 @@ Also, there are additional jobs for person (face) thumbnails.
There are no requirements for assets to be unique across users. If multiple users upload the same image they are processed as if they were distinct assets and jobs run and thumbnails are generated accordingly. There are no requirements for assets to be unique across users. If multiple users upload the same image they are processed as if they were distinct assets and jobs run and thumbnails are generated accordingly.
### Why do HDR videos appear pale in Immich player but look normal after download?
Immich uses a player with known HDR color display issues. We are experimenting with a different player that provides better color profiles for HDR content for future improvements.
### How can I delete transcoded videos without deleting the original? ### How can I delete transcoded videos without deleting the original?
The transcode of an asset can be deleted by setting a transcode policy that makes it unnecessary, then running a transcoding job for that asset. This can be done on a per-asset basis by starting a transcoding job for an asset (with the _Refresh encoded videos_ button in the asset viewer options. Or, for all assets by running transcoding jobs for all assets. The transcode of an asset can be deleted by setting a transcode policy that makes it unnecessary, then running a transcoding job for that asset. This can be done on a per-asset basis by starting a transcoding job for an asset (with the _Refresh encoded videos_ button in the asset viewer options. Or, for all assets by running transcoding jobs for all assets.
@@ -214,11 +210,6 @@ On the other hand, Immich does scan video thumbnails for faces, so it can perfor
No. No.
### I'm getting a lot of "faces" that aren't faces, what can I do?
You can increase the MIN DETECTION SCORE to 0.8 to help prevent bad thumbnails. However, a score of 0.9 might filter out too many real faces depending on the library used. If you just want to hide specific faces, you can adjust the 'MIN FACES DETECTED' setting in the administration panel
to increase the bar for what the algorithm considers a "core face" for that person, reducing the chance of bad thumbnails being chosen.
### The immich_model-cache volume takes up a lot of space, what could be the problem? ### The immich_model-cache volume takes up a lot of space, what could be the problem?
If you installed several models and chose not to use some of them, it might be worth deleting the old models that are in immich_model-cache. If you installed several models and chose not to use some of them, it might be worth deleting the old models that are in immich_model-cache.
@@ -295,7 +286,7 @@ The non-root user/group needs read/write access to the volume mounts, including
Data for Immich comes in two forms: Data for Immich comes in two forms:
1. **Metadata** stored in a postgres database, persisted via the `pg_data` volume 1. **Metadata** stored in a postgres database, persisted via the `pg_data` volume
2. **Files** (originals, thumbs, profile, etc.), stored in the `UPLOAD_LOCATION` folder, more [info](/docs/administration/backup-and-restore#asset-types-and-storage-locations). 2. **Files** (originals, thumbs, profile, etc.), stored in the `UPLOAD_LOCATION` folder.
To remove the **Metadata** you can stop Immich and delete the volume. To remove the **Metadata** you can stop Immich and delete the volume.
@@ -303,11 +294,6 @@ To remove the **Metadata** you can stop Immich and delete the volume.
docker compose down -v docker compose down -v
``` ```
:::note Portainer
If you use portainer, bring down the stack in portainer. Go into the volumes section
and remove all the volumes related to immcih then restart the stack.
:::
After removing the containers and volumes, the **Files** can be cleaned up (if necessary) from the `UPLOAD_LOCATION` by simply deleting any unwanted files or folders. After removing the containers and volumes, the **Files** can be cleaned up (if necessary) from the `UPLOAD_LOCATION` by simply deleting any unwanted files or folders.
### Why does the machine learning service report workers crashing? ### Why does the machine learning service report workers crashing?
@@ -323,7 +309,3 @@ If the error mentions SIGKILL or error code 137, it most likely means the servic
If it mentions SIGILL (note the lack of a K) or error code 132, it most likely means your server's CPU is incompatible. This is unlikely to occur on version 1.92.0 or later. Consider upgrading if your version of Immich is below that. If it mentions SIGILL (note the lack of a K) or error code 132, it most likely means your server's CPU is incompatible. This is unlikely to occur on version 1.92.0 or later. Consider upgrading if your version of Immich is below that.
If your version of Immich is below 1.92.0 and the crash occurs after logs about tracing or exporting a model, consider either upgrading or disabling the Tag Objects job. If your version of Immich is below 1.92.0 and the crash occurs after logs about tracing or exporting a model, consider either upgrading or disabling the Tag Objects job.
### Why do I get the error "duplicate key value violates unique constraint" in the log files?
Because of Immich's container structure, this error can be seen when both immich and immich-microservices start at the same time and attempt to migrate or create the database structure. Since the database migration is run sequentially and inside of transactions, this error message does not cause harm to your installation of Immich and can safely be ignored. If needed, you can manually restart Immich by running `docker restart immich immich-microservices`.
+12 -82
View File
@@ -1,8 +1,5 @@
# Backup and Restore # Backup and Restore
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
A [3-2-1 backup strategy](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) is recommended to protect your data. You should keep copies of your uploaded photos/videos as well as the Immich database for a comprehensive backup solution. This page provides an overview on how to backup the database and the location of user-uploaded pictures and videos. A template bash script that can be run as a cron job is provided [here](/docs/guides/template-backup-script.md) A [3-2-1 backup strategy](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) is recommended to protect your data. You should keep copies of your uploaded photos/videos as well as the Immich database for a comprehensive backup solution. This page provides an overview on how to backup the database and the location of user-uploaded pictures and videos. A template bash script that can be run as a cron job is provided [here](/docs/guides/template-backup-script.md)
## Database ## Database
@@ -17,10 +14,7 @@ Refer to the official [postgres documentation](https://www.postgresql.org/docs/c
The recommended way to backup and restore the Immich database is to use the `pg_dumpall` command. The recommended way to backup and restore the Immich database is to use the `pg_dumpall` command.
<Tabs> ```bash title='Backup'
<TabItem value="Linux system based Backup" label="Linux system based Backup" default>
```bash title='Bash'
docker exec -t immich_postgres pg_dumpall -c -U postgres | gzip > "/path/to/backup/dump.sql.gz" docker exec -t immich_postgres pg_dumpall -c -U postgres | gzip > "/path/to/backup/dump.sql.gz"
``` ```
@@ -34,26 +28,6 @@ gunzip < "/path/to/backup/dump.sql.gz" | docker exec -i immich_postgres psql -U
docker compose up -d # Start remainder of Immich apps docker compose up -d # Start remainder of Immich apps
``` ```
</TabItem>
<TabItem value="Windows system based Backup" label="Windows system based Backup">
```powershell title='Backup'
docker exec -t immich_postgres pg_dumpall -c -U postgres > "\path\to\backup\dump.sql"
```
```powershell title='Restore'
docker compose down -v # CAUTION! Deletes all Immich data to start from scratch.
docker compose pull # Update to latest version of Immich (if desired)
docker compose create # Create Docker containers for Immich apps without running them.
docker start immich_postgres # Start Postgres server
sleep 10 # Wait for Postgres server to start up
gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql -U postgres -d immich # Restore Backup
docker compose up -d # Start remainder of Immich apps
```
</TabItem>
</Tabs>
Note that for the database restore to proceed properly, it requires a completely fresh install (i.e. the Immich server has never run since creating the Docker containers). If the Immich app has run, Postgres conflicts may be encountered upon database restoration (relation already exists, violated foreign key constraints, multiple primary keys, etc.). Note that for the database restore to proceed properly, it requires a completely fresh install (i.e. the Immich server has never run since creating the Docker containers). If the Immich app has run, Postgres conflicts may be encountered upon database restoration (relation already exists, violated foreign key constraints, multiple primary keys, etc.).
The database dumps can also be automated (using [this image](https://github.com/prodrigestivill/docker-postgres-backup-local)) by editing the docker compose file to match the following: The database dumps can also be automated (using [this image](https://github.com/prodrigestivill/docker-postgres-backup-local)) by editing the docker compose file to match the following:
@@ -90,78 +64,34 @@ gunzip < db_dumps/last/immich-latest.sql.gz | docker exec -i immich_postgres psq
Immich stores two types of content in the filesystem: (1) original, unmodified content, and (2) generated content. Only the original content needs to be backed-up, which includes the following folders: Immich stores two types of content in the filesystem: (1) original, unmodified content, and (2) generated content. Only the original content needs to be backed-up, which includes the following folders:
1. `UPLOAD_LOCATION/library` 1. `UPLOAD_LOCATION/library`
2. `UPLOAD_LOCATION/upload` 1. `UPLOAD_LOCATION/upload`
3. `UPLOAD_LOCATION/profile` 1. `UPLOAD_LOCATION/profile`
### Asset Types and Storage Locations
Some storage locations are impacted by the Storage Template. See below for more details.
<Tabs>
<TabItem value="Storage Template Off (Default)." label="Storage Template Off (Default)." default>
:::note
`UPLOAD_LOCATION/library` folder is not used by default on new machines running version 1.92.0. These are if the system administrator activated the storage template engine, for [more info](https://github.com/immich-app/immich/releases#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further).
:::
**1. User-Specific Folders:** **1. User-Specific Folders:**
- Each user has a unique string representing them. - Each user has a unique string representing them.
- You can find your user ID in Account Account Settings -> Account -> User ID. - The main user is "Admin" (but only for `\library\library\`)
**2. Asset Types and Storage Locations:**
- **Source Assets:**
- Original assets uploaded through the browser interface & mobile & CLI.
- Stored in `/library/upload/<userID>`.
- **Avatar Images:**
- User profile images.
- Stored in `/library/profile/<userID>`.
- **Thumbs Images:**
- Preview images (blurred, small, large) for each asset and thumbnails for recognized faces.
- Stored in `/library/thumbs/<userID>`.
- **Encoded Assets:**
- By default, unless otherwise specified re-encoded video assets for wider compatibility.
- Stored in `/library/encoded-video/<userID>`.
</TabItem>
<TabItem value="Storage Template On" label="Storage Template On">
:::note
If you choose to activate the storage template engine, it will move all assets to `UPLOAD_LOCATION/library/<userID>`.
When you turn off the storage template engine, it will leave the assets in `UPLOAD_LOCATION/library/<userID>` and will not return them to `/library/upload`.
**New assets** will be saved to `/library/upload`.
:::
**1. User-Specific Folders:**
- Each user has a unique string representing them.
- The main user is "Admin" (but only for `UPLOAD_LOCATION/library`)
- Other users have different string identifiers. - Other users have different string identifiers.
- You can find your user ID in Account Account Settings -> Account -> User ID. - You can find your user ID in Account Account Settings > Account > User ID.
**2. Asset Types and Storage Locations:** **2. Asset Types and Storage Locations:**
- **Source Assets:** - **Source Assets:**
- Original assets uploaded through the browser interface & mobile & CLI. - Original assets uploaded through the browser interface&mobile&CLI.
- Stored in `UPLOAD_LOCATION/library/<userID>`. - Stored in `\library\library\<userID>`.
- **Avatar Images:** - **Avatar Images:**
- User profile images. - User profile images.
- Stored in `/library/profile/<userID>`. - Stored in `\library\profile\<userID>`.
- **Thumbs Images:** - **Thumbs Images:**
- Preview images (blurred, small, large) for each asset and thumbnails for recognized faces. - Preview images (blurred, small, large) for each asset and thumbnails for recognized faces.
- Stored in `/library/thumbs/<userID>`. - Stored in `\library\thumbs\<userID>`.
- **Encoded Assets:** - **Encoded Assets:**
- By default, unless otherwise specified re-encoded video assets for wider compatibility . - By default, unless otherwise specified re-encoded video assets for wider compatibility .
- Stored in `/library/encoded-video/<userID>`. - Stored in `\library\encoded-video\<userID>`.
- **Files in Upload Queue (Mobile):** - **Files in Upload Queue (Mobile):**
- Files uploaded through mobile apps. - Files uploaded through mobile apps.
- Temporarily located in `/library/upload/<userID>`. - Temporarily located in `\library\upload\<userID>`.
- Transferred to `UPLOAD_LOCATION/library/<userID>` upon successful upload. - Transferred to `\library\library\<userID>` upon successful upload.
</TabItem>
</Tabs>
:::danger :::danger
Do not touch the files inside these folders under any circumstances except taking a backup, changing or removing an asset can cause untracked and missing files. Do not touch the files inside these folders under any circumstances except taking a backup, changing or removing an asset can cause untracked and missing files.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 501 KiB

-26
View File
@@ -38,29 +38,3 @@ immich.example.org {
reverse_proxy http://<snip>:2283 reverse_proxy http://<snip>:2283
} }
``` ```
### Apache example config
Below is an example config for Apache2 site configuration.
```
<VirtualHost *:80>
ServerName <snip>
ProxyRequests off
ProxyVia on
RewriteEngine On
RewriteCond %{REQUEST_URI} ^/api/socket.io [NC]
RewriteCond %{QUERY_STRING} transport=websocket [NC]
RewriteRule /(.*) ws://localhost:2283/$1 [P,L]
ProxyPass /api/socket.io ws://localhost:2283/api/socket.io
ProxyPassReverse /api/socket.io ws://localhost:2283/api/socket.io
<Location />
ProxyPass http://localhost:2283/
ProxyPassReverse http://localhost:2283/
</Location>
</VirtualHost>
```
+4 -9
View File
@@ -8,15 +8,10 @@ Unit are run by calling `npm run test` from the `server` directory.
### End to end tests ### End to end tests
The backend has two end-to-end test suites that can be called with the following two commands from the project root directory: The backend has an end-to-end test suite that can be called with `npm run test:e2e` from the `server` directory. This will set up a dummy database inside a temporary container and run the tests against it. Setup and teardown is automatically taken care of. That test, however, can not set up all prerequisites to parse file formats, as that is very complex and error-prone. As such, this test excludes some test cases like HEIC file imports. The test suite will also print a friendly warning to remind you that not all tests are being run.
- `make server-e2e-api` Note that there is a bug in nodejs \<20.8 that causes segmentation faults when running these tests. If you run into segfaults, ensure you are using at least version 20.8.
- `make server-e2e-jobs`
#### API (e2e) To perform a full e2e test, you need to run e2e tests inside docker. The easiest way to do that is to run `make test-e2e` in the root directory. This will build and start a docker-compose consisting of the server, microservices, and a postgres database. It will then perform the tests and exit.
The API e2e tests spin up a test database and execute http requests against the server, validating the expected response codes and functionality for API endpoints. If you manually install the dependencies (see the DOCKERFILE) on your development machine, you can also run the full e2e tests manually by setting the `IMMICH_RUN_ALL_TESTS` environment value to true, i.e. `IMMICH_RUN_ALL_TESTS=true npm run e2e:jobs`.
#### Jobs (e2e)
The Jobs e2e tests spin up a docker test environment where thumbnail generation, library scanning, and other _job_ workflows are validated.
+5 -19
View File
@@ -52,11 +52,8 @@ Sometimes, an external library will not scan correctly. This can happen if the i
- 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?
- Are you using symbolic link in your import library?
- Are the permissions set correctly? - Are the permissions set correctly?
- Are you using forward slashes everywhere? (`/`) - Are you using forward slashes everywhere? (`/`)
- Are you using symlink across docker mounts?
- Are you using [spaces in the internal path](/docs/features/libraries#:~:text=can%20be%20accessed.-,NOTE,-Spaces%20in%20the)?
If all else fails, you can always start a shell inside the container and check if the path is accessible. For example, `docker exec -it immich_microservices /bin/bash` will start a bash shell. If your import path, for instance, is `/data/import/photos`, you can check if the files are accessible by running `ls /data/import/photos`. Also check the `immich_server` container in the same way. If all else fails, you can always start a shell inside the container and check if the path is accessible. For example, `docker exec -it immich_microservices /bin/bash` will start a bash shell. If your import path, for instance, is `/data/import/photos`, you can check if the files are accessible by running `ls /data/import/photos`. Also check the `immich_server` container in the same way.
@@ -72,25 +69,16 @@ For security purposes, each Immich user is disallowed to add external files by d
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. 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 and Scan Settings
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.
Some basic examples: Some basic examples:
- `**/*.tif` will exclude all files with the extension `.tif` - `*.tif` will exclude all files with the extension `.tif`
- `**/hidden.jpg` will exclude all files named `hidden.jpg` - `hidden.jpg` will exclude all files named `hidden.jpg`
- `**/Raw/**` will exclude all files in any directory named `Raw` - `**/Raw/**` will exclude all files in any directory named `Raw`
- `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg` - `*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
### Automatic watching (EXPERIMENTAL)
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 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.
- `usePolling` (default: `false`).
- `interval`. (default: 10000). When using polling, this is how often (in milliseconds) the filesystem is polled.
### Nightly job ### Nightly job
@@ -133,9 +121,7 @@ First, we need to plan how we want to organize the libraries. The christmas trip
The `ro` flag at the end only gives read-only access to the volumes. While Immich does not modify files, it's a good practice to mount read-only. The `ro` flag at the end only gives read-only access to the volumes. While Immich does not modify files, it's a good practice to mount read-only.
::: :::
:::info _Remember to bring the container 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 ### Set External Path
+2 -11
View File
@@ -4,17 +4,8 @@ A short guide on connecting [pgAdmin](https://www.pgadmin.org/) to Immich.
:::note :::note
In order to connect to the database the immich_postgres container **must be running**. - In order to connect to the database the immich_postgres container **must be running**.
- The passwords and usernames used below match the ones specified in the example `.env` file. If changed, please use actual values instead.
The passwords and usernames used below match the ones specified in the example `.env` file. If changed, please use actual values instead.
**Optional:** To connect to the database **outside** of your Docker's network:
- Expose port 5432 in your `docker-compose.yml` file.
- Edit the PostgreSQL [`pg_hba.conf`](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html) file.
- Make sure your firewall does not block access to port 5432.
Note that exposing the database port increases the risk of getting attacked by hackers.
Make sure to remove the binding port after finishing the database's tasks.
::: :::
+1 -1
View File
@@ -7,7 +7,7 @@ Keep in mind that mucking around in the database might set the moon on fire. Avo
:::tip :::tip
Run `docker exec -it immich_postgres psql immich <DB_USERNAME>` to connect to the database via the container directly. Run `docker exec -it immich_postgres psql immich <DB_USERNAME>` to connect to the database via the container directly.
(Replace `<DB_USERNAME>` with the value from your [`.env` file](/docs/install/environment-variables#database)). (Replace `<DB_USERNAME>` wit the value from your [`.env` file](/docs/install/environment-variables#database)).
::: :::
## Assets ## Assets
@@ -6,10 +6,6 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
- Copy the following `docker-compose.yml` to your ML system. - Copy the following `docker-compose.yml` to your ML system.
- Start the container by running `docker-compose up -d` or `docker compose up -d` (depending on your Docker version). - Start the container by running `docker-compose up -d` or `docker compose up -d` (depending on your Docker version).
:::note Info
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning service, but facial recognition is done in the immich_microservices service.
:::
```yaml ```yaml
version: '3.8' version: '3.8'
+4 -142
View File
@@ -1,146 +1,11 @@
# Remove Offline Files [Community] # Remove Offline Files
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
:::note :::note
**Before running the script**, please make sure you have a [backup](/docs/administration/backup-and-restore) of your assets and database. **Before running the script**, please make sure you have a [backup](/docs/administration/backup-and-restore) of your assets and database
:::
:::info
**None** of the scripts can delete orphaned files from the external library.
::: :::
This page is a guide to get rid of offline files from the repair page. This page is a guide to get rid of offline files from the repair page.
<Tabs>
<TabItem value="Python script (Best way)" label="Python script (Best way)">
This way works by retrieving a file that contains a list of all the files that are defined as offline files, running a script that uses the [Immich API](/docs/api/delete-assets) in order to remove the offline files.
1. Create an API key under Admin User -> Account Settings -> API Keys -> New API Key -> Copy to clipboard.
2. Copy and save the code to file -> `Immich Remove Offline Files.py`.
3. Run the script and follow the instructions.
:::note
You might need to run `pip install halo tabulate tqdm` if these dependencies are missing on your machine.
:::
```bash title='Python'
#!/usr/bin/env python3
# Note: you might need to run "pip install halo tabulate tqdm" if these dependencies are missing on your machine
import argparse
import json
import requests
from datetime import datetime
from halo import Halo
from tabulate import tabulate
from tqdm import tqdm
from urllib.parse import urlparse
def parse_arguments():
parser = argparse.ArgumentParser(description='Fetch file report and delete orphaned media assets from Immich.')
parser.add_argument('--apikey', help='Immich API key for authentication')
parser.add_argument('--immichaddress', help='Full address for Immich, including protocol and port')
parser.add_argument('--no_prompt', action='store_true', help='Delete orphaned media assets without confirmation')
args = parser.parse_args()
return args
def filter_entities(response_json, entity_type):
return [
{'pathValue': entity['pathValue'], 'entityId': entity['entityId'], 'entityType': entity['entityType']}
for entity in response_json.get('orphans', []) if entity.get('entityType') == entity_type
]
def main():
args = parse_arguments()
try:
if args.apikey:
api_key = args.apikey
else:
api_key = input('Enter the Immich API key: ')
if args.immichaddress:
immich_server = args.immichaddress
else:
immich_server = input('Enter the full web address for Immich, including protocol and port: ')
immich_parsed_url = urlparse(immich_server)
base_url = f'{immich_parsed_url.scheme}://{immich_parsed_url.netloc}'
api_url = f'{base_url}/api'
file_report_url = api_url + '/audit/file-report'
headers = {'x-api-key': api_key}
print()
spinner = Halo(text='Retrieving list of orphaned media assets...', spinner='dots')
spinner.start()
try:
response = requests.get(file_report_url, headers=headers)
response.raise_for_status()
spinner.succeed('Success!')
except requests.exceptions.RequestException as e:
spinner.fail(f'Failed to fetch assets: {str(e)}')
person_assets = filter_entities(response.json(), 'person')
orphan_media_assets = filter_entities(response.json(), 'asset')
num_entries = len(orphan_media_assets)
if num_entries == 0:
print('No orphaned media assets found; exiting.')
return
else:
if not args.no_prompt:
table_data = []
for asset in orphan_media_assets:
table_data.append([asset['pathValue'], asset['entityId']])
print(tabulate(table_data, headers=['Path Value', 'Entity ID'], tablefmt='pretty'))
print()
if person_assets:
print('Found orphaned person assets! Please run the "RECOGNIZE FACES > ALL" job in Immich after running this tool to correct this.')
print()
if num_entries > 0:
summary = f'There {"is" if num_entries == 1 else "are"} {num_entries} orphaned media asset{"s" if num_entries != 1 else ""}. Would you like to delete {"them" if num_entries != 1 else "it"} from Immich? (yes/no): '
user_input = input(summary).lower()
print()
if user_input not in ('y', 'yes'):
print('Exiting without making any changes.')
return
with tqdm(total=num_entries, desc="Deleting orphaned media assets", unit="asset") as progress_bar:
for asset in orphan_media_assets:
entity_id = asset['entityId']
asset_url = f'{api_url}/asset'
delete_payload = json.dumps({'force': True, 'ids': [entity_id]})
headers = {'Content-Type': 'application/json', 'x-api-key': api_key}
response = requests.delete(asset_url, headers=headers, data=delete_payload)
response.raise_for_status()
progress_bar.set_postfix_str(entity_id)
progress_bar.update(1)
print()
print('Orphaned media assets deleted successfully!')
except Exception as e:
print()
print(f"An error occurred: {str(e)}")
if __name__ == '__main__':
main()
```
Thanks to [DooMRunneR](https://discord.com/channels/979116623879368755/1179655214870040596/1194308198413373482) and [Sircharlo](https://discord.com/channels/979116623879368755/1179655214870040596/1195038609812758639) for writing this script.
</TabItem>
<TabItem value="Bash and PowerShell script" label="Bash and PowerShell script" default>
This way works by downloading a JSON file that contains a list of all the files that are defined as offline files, running a script that uses the [Immich API](/docs/api/delete-assets) in order to remove the offline files. This way works by downloading a JSON file that contains a list of all the files that are defined as offline files, running a script that uses the [Immich API](/docs/api/delete-assets) in order to remove the offline files.
1. Create an API key under Admin User -> Account Settings -> API Keys -> New API Key -> Copy to clipboard. 1. Create an API key under Admin User -> Account Settings -> API Keys -> New API Key -> Copy to clipboard.
@@ -150,13 +15,13 @@ This way works by downloading a JSON file that contains a list of all the files
## Script for Linux based systems: ## Script for Linux based systems:
```bash title='Bash' ```bash
awk -F\" '/entityId/ {print $4}' orphans.json | while read line; do curl --location --request DELETE 'http://YOUR_IP_HERE:2283/api/asset' --header 'Content- Type: application/json' --header 'x-api-key: YOUR_API_KEY_HERE' --data '{ "force": true, "ids": ["'"$line"'"]}';done awk -F\" '/entityId/ {print $4}' orphans.json | while read line; do curl --location --request DELETE 'http://YOUR_IP_HERE:2283/api/asset' --header 'Content- Type: application/json' --header 'x-api-key: YOUR_API_KEY_HERE' --data '{ "force": true, "ids": ["'"$line"'"]}';done
``` ```
## Script for the Windows system (run through PowerShell): ## Script for the Windows system (run through PowerShell):
```powershell title='PowerShell' ```powershell
Get-Content orphans.json | Select-String -Pattern 'entityId' | ForEach-Object { Get-Content orphans.json | Select-String -Pattern 'entityId' | ForEach-Object {
$line = $_ -split '"' | Select-Object -Index 3 $line = $_ -split '"' | Select-Object -Index 3
$body = [pscustomobject]@{ $body = [pscustomobject]@{
@@ -171,6 +36,3 @@ Get-Content orphans.json | Select-String -Pattern 'entityId' | ForEach-Object {
``` ```
Thanks to [DooMRunneR](https://discord.com/channels/979116623879368755/1179655214870040596/1194308198413373482) for writing this script. Thanks to [DooMRunneR](https://discord.com/channels/979116623879368755/1179655214870040596/1194308198413373482) for writing this script.
</TabItem>
</Tabs>
-5
View File
@@ -129,11 +129,6 @@ The default configuration looks like this:
"scan": { "scan": {
"enabled": true, "enabled": true,
"cronExpression": "0 0 * * *" "cronExpression": "0 0 * * *"
},
"watch": {
"enabled": false,
"usePolling": false,
"interval": 10000
} }
} }
} }
-12
View File
@@ -1,13 +1,5 @@
Immich allows the admin user to set the uploaded filename pattern. Both at the directory and filename level. Immich allows the admin user to set the uploaded filename pattern. Both at the directory and filename level.
:::note new version
On new machines running version 1.92.0 storage template engine is off by default, for [more info](https://github.com/immich-app/immich/releases#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further).
:::
:::tip
You can read more about the differences between storage template engine on and off [here](/docs/administration/backup-and-restore#asset-types-and-storage-locations)
:::
The admin user can set the template by using the template builder in the `Administration -> Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text. If the template produces [multiple files with the same filename, they won't be overwritten](https://github.com/immich-app/immich/discussions/3324) as a sequence number is appended to the filename. The admin user can set the template by using the template builder in the `Administration -> Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text. If the template produces [multiple files with the same filename, they won't be overwritten](https://github.com/immich-app/immich/discussions/3324) as a sequence number is appended to the filename.
```bash title="Default template" ```bash title="Default template"
@@ -16,8 +8,4 @@ Year/Year-Month-Day/Filename.Extension
<img src={require('./img/storage-template.png').default} width="100%" title="Storage Template Setting" /> <img src={require('./img/storage-template.png').default} width="100%" title="Storage Template Setting" />
:::tip
By default, special characters will be converted to an HTML entity (for example, `&` -> `&amp;`). To prevent this, wrap the variable in an extra set of braces (for example, `{{{album}}}`). You can learn more about this [here](https://handlebarsjs.com/guide/expressions.html#html-escaping) and [here](https://github.com/immich-app/immich/issues/4917).
:::
Immich also provides a mechanism to migrate between templates so that if the template you set now doesn't work in the future, you can always migrate all the existing files to the new template. The mechanism is run as a job on the Job page. Immich also provides a mechanism to migrate between templates so that if the template you set now doesn't work in the future, you can always migrate all the existing files to the new template. The mechanism is run as a job on the Job page.
+3650 -2321
View File
File diff suppressed because it is too large Load Diff
-20
View File
@@ -1,5 +1,4 @@
import { import {
mdiEyeRefreshOutline,
mdiAccountGroup, mdiAccountGroup,
mdiAndroid, mdiAndroid,
mdiAppleIos, mdiAppleIos,
@@ -12,7 +11,6 @@ import {
mdiCollage, mdiCollage,
mdiContentCopy, mdiContentCopy,
mdiDevices, mdiDevices,
mdiExpansionCard,
mdiFaceMan, mdiFaceMan,
mdiFaceManOutline, mdiFaceManOutline,
mdiFile, mdiFile,
@@ -56,24 +54,6 @@ 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: mdiEyeRefreshOutline,
description: 'Automatically import files in external libraries when the operating system detects changes.',
title: 'Library watching',
release: 'v1.94.0',
tag: 'v1.94.0',
date: new Date(2024, 0, 31),
dateType: DateType.RELEASE,
},
{
icon: mdiExpansionCard,
description: 'Hardware acceleration support for Nvidia and Intel devices through CUDA and OpenVINO.',
title: 'GPU acceleration for machine-learning',
release: 'v1.94.0',
tag: 'v1.94.0',
date: new Date(2024, 0, 31),
dateType: DateType.RELEASE,
},
{ {
icon: mdiMatrix, icon: mdiMatrix,
description: 'Moved the search from typesense to pgvecto.rs', description: 'Moved the search from typesense to pgvecto.rs',
+256
View File
@@ -0,0 +1,256 @@
import Hogan from 'hogan.js';
import LunrSearchAdapter from './lunar-search';
import autocomplete from 'autocomplete.js';
import templates from './templates';
import utils from './utils';
import $ from 'autocomplete.js/zepto';
class DocSearch {
constructor({
searchDocs,
searchIndex,
inputSelector,
debug = false,
baseUrl = '/',
queryDataCallback = null,
autocompleteOptions = {
debug: false,
hint: false,
autoselect: true,
},
transformData = false,
queryHook = false,
handleSelected = false,
enhancedSearchInput = false,
layout = 'collumns',
}) {
this.input = DocSearch.getInputFromSelector(inputSelector);
this.queryDataCallback = queryDataCallback || null;
const autocompleteOptionsDebug =
autocompleteOptions && autocompleteOptions.debug ? autocompleteOptions.debug : false;
// eslint-disable-next-line no-param-reassign
autocompleteOptions.debug = debug || autocompleteOptionsDebug;
this.autocompleteOptions = autocompleteOptions;
this.autocompleteOptions.cssClasses = this.autocompleteOptions.cssClasses || {};
this.autocompleteOptions.cssClasses.prefix = this.autocompleteOptions.cssClasses.prefix || 'ds';
const inputAriaLabel = this.input && typeof this.input.attr === 'function' && this.input.attr('aria-label');
this.autocompleteOptions.ariaLabel = this.autocompleteOptions.ariaLabel || inputAriaLabel || 'search input';
this.isSimpleLayout = layout === 'simple';
this.client = new LunrSearchAdapter(searchDocs, searchIndex, baseUrl);
if (enhancedSearchInput) {
this.input = DocSearch.injectSearchBox(this.input);
}
this.autocomplete = autocomplete(this.input, autocompleteOptions, [
{
source: this.getAutocompleteSource(transformData, queryHook),
templates: {
suggestion: DocSearch.getSuggestionTemplate(this.isSimpleLayout),
footer: templates.footer,
empty: DocSearch.getEmptyTemplate(),
},
},
]);
const customHandleSelected = handleSelected;
this.handleSelected = customHandleSelected || this.handleSelected;
// We prevent default link clicking if a custom handleSelected is defined
if (customHandleSelected) {
$('.algolia-autocomplete').on('click', '.ds-suggestions a', (event) => {
event.preventDefault();
});
}
this.autocomplete.on('autocomplete:selected', this.handleSelected.bind(null, this.autocomplete.autocomplete));
this.autocomplete.on('autocomplete:shown', this.handleShown.bind(null, this.input));
if (enhancedSearchInput) {
DocSearch.bindSearchBoxEvent();
}
}
static injectSearchBox(input) {
input.before(templates.searchBox);
const newInput = input.prev().prev().find('input');
input.remove();
return newInput;
}
static bindSearchBoxEvent() {
$('.searchbox [type="reset"]').on('click', function () {
$('input#docsearch').focus();
$(this).addClass('hide');
autocomplete.autocomplete.setVal('');
});
$('input#docsearch').on('keyup', () => {
const searchbox = document.querySelector('input#docsearch');
const reset = document.querySelector('.searchbox [type="reset"]');
reset.className = 'searchbox__reset';
if (searchbox.value.length === 0) {
reset.className += ' hide';
}
});
}
/**
* Returns the matching input from a CSS selector, null if none matches
* @function getInputFromSelector
* @param {string} selector CSS selector that matches the search
* input of the page
* @returns {void}
*/
static getInputFromSelector(selector) {
const input = $(selector).filter('input');
return input.length ? $(input[0]) : null;
}
/**
* Returns the `source` method to be passed to autocomplete.js. It will query
* the Algolia index and call the callbacks with the formatted hits.
* @function getAutocompleteSource
* @param {function} transformData An optional function to transform the hits
* @param {function} queryHook An optional function to transform the query
* @returns {function} Method to be passed as the `source` option of
* autocomplete
*/
getAutocompleteSource(transformData, queryHook) {
return (query, callback) => {
if (queryHook) {
// eslint-disable-next-line no-param-reassign
query = queryHook(query) || query;
}
this.client.search(query).then((hits) => {
if (this.queryDataCallback && typeof this.queryDataCallback == 'function') {
this.queryDataCallback(hits);
}
if (transformData) {
hits = transformData(hits) || hits;
}
callback(DocSearch.formatHits(hits));
});
};
}
// Given a list of hits returned by the API, will reformat them to be used in
// a Hogan template
static formatHits(receivedHits) {
const clonedHits = utils.deepClone(receivedHits);
const hits = clonedHits.map((hit) => {
if (hit._highlightResult) {
// eslint-disable-next-line no-param-reassign
hit._highlightResult = utils.mergeKeyWithParent(hit._highlightResult, 'hierarchy');
}
return utils.mergeKeyWithParent(hit, 'hierarchy');
});
// Group hits by category / subcategory
let groupedHits = utils.groupBy(hits, 'lvl0');
$.each(groupedHits, (level, collection) => {
const groupedHitsByLvl1 = utils.groupBy(collection, 'lvl1');
const flattenedHits = utils.flattenAndFlagFirst(groupedHitsByLvl1, 'isSubCategoryHeader');
groupedHits[level] = flattenedHits;
});
groupedHits = utils.flattenAndFlagFirst(groupedHits, 'isCategoryHeader');
// Translate hits into smaller objects to be send to the template
return groupedHits.map((hit) => {
const url = DocSearch.formatURL(hit);
const category = utils.getHighlightedValue(hit, 'lvl0');
const subcategory = utils.getHighlightedValue(hit, 'lvl1') || category;
const displayTitle = utils
.compact([
utils.getHighlightedValue(hit, 'lvl2') || subcategory,
utils.getHighlightedValue(hit, 'lvl3'),
utils.getHighlightedValue(hit, 'lvl4'),
utils.getHighlightedValue(hit, 'lvl5'),
utils.getHighlightedValue(hit, 'lvl6'),
])
.join('<span class="aa-suggestion-title-separator" aria-hidden="true"> </span>');
const text = utils.getSnippetedValue(hit, 'content');
const isTextOrSubcategoryNonEmpty = (subcategory && subcategory !== '') || (displayTitle && displayTitle !== '');
const isLvl1EmptyOrDuplicate = !subcategory || subcategory === '' || subcategory === category;
const isLvl2 = displayTitle && displayTitle !== '' && displayTitle !== subcategory;
const isLvl1 = !isLvl2 && subcategory && subcategory !== '' && subcategory !== category;
const isLvl0 = !isLvl1 && !isLvl2;
return {
isLvl0,
isLvl1,
isLvl2,
isLvl1EmptyOrDuplicate,
isCategoryHeader: hit.isCategoryHeader,
isSubCategoryHeader: hit.isSubCategoryHeader,
isTextOrSubcategoryNonEmpty,
category,
subcategory,
title: displayTitle,
text,
url,
};
});
}
static formatURL(hit) {
const { url, anchor } = hit;
if (url) {
const containsAnchor = url.indexOf('#') !== -1;
if (containsAnchor) return url;
else if (anchor) return `${hit.url}#${hit.anchor}`;
return url;
} else if (anchor) return `#${hit.anchor}`;
/* eslint-disable */
console.warn('no anchor nor url for : ', JSON.stringify(hit));
/* eslint-enable */
return null;
}
static getEmptyTemplate() {
return (args) => Hogan.compile(templates.empty).render(args);
}
static getSuggestionTemplate(isSimpleLayout) {
const stringTemplate = isSimpleLayout ? templates.suggestionSimple : templates.suggestion;
const template = Hogan.compile(stringTemplate);
return (suggestion) => template.render(suggestion);
}
handleSelected(input, event, suggestion, datasetNumber, context = {}) {
// Do nothing if click on the suggestion, as it's already a <a href>, the
// browser will take care of it. This allow Ctrl-Clicking on results and not
// having the main window being redirected as well
if (context.selectionMethod === 'click') {
return;
}
input.setVal('');
window.location.assign(suggestion.url);
}
handleShown(input) {
const middleOfInput = input.offset().left + input.width() / 2;
let middleOfWindow = $(document).width() / 2;
if (isNaN(middleOfWindow)) {
middleOfWindow = 900;
}
const alignClass = middleOfInput - middleOfWindow >= 0 ? 'algolia-autocomplete-right' : 'algolia-autocomplete-left';
const otherAlignClass =
middleOfInput - middleOfWindow < 0 ? 'algolia-autocomplete-right' : 'algolia-autocomplete-left';
const autocompleteWrapper = $('.algolia-autocomplete');
if (!autocompleteWrapper.hasClass(alignClass)) {
autocompleteWrapper.addClass(alignClass);
}
if (autocompleteWrapper.hasClass(otherAlignClass)) {
autocompleteWrapper.removeClass(otherAlignClass);
}
}
}
export default DocSearch;
File diff suppressed because one or more lines are too long
+111
View File
@@ -0,0 +1,111 @@
import React, { useRef, useCallback, useState } from 'react';
import classnames from 'classnames';
import { useHistory } from '@docusaurus/router';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { usePluginData } from '@docusaurus/useGlobalData';
import useIsBrowser from '@docusaurus/useIsBrowser';
const Search = (props) => {
const initialized = useRef(false);
const searchBarRef = useRef(null);
const [indexReady, setIndexReady] = useState(false);
const history = useHistory();
const { siteConfig = {} } = useDocusaurusContext();
const isBrowser = useIsBrowser();
const { baseUrl } = siteConfig;
const initAlgolia = (searchDocs, searchIndex, DocSearch) => {
new DocSearch({
searchDocs,
searchIndex,
baseUrl,
inputSelector: '#search_input_react',
// Override algolia's default selection event, allowing us to do client-side
// navigation and avoiding a full page refresh.
handleSelected: (_input, _event, suggestion) => {
const url = suggestion.url || '/';
// Use an anchor tag to parse the absolute url into a relative url
// Alternatively, we can use new URL(suggestion.url) but its not supported in IE
const a = document.createElement('a');
a.href = url;
// Algolia use closest parent element id #__docusaurus when a h1 page title does not have an id
// So, we can safely remove it. See https://github.com/facebook/docusaurus/issues/1828 for more details.
history.push(url);
},
});
};
const pluginData = usePluginData('docusaurus-lunr-search');
const getSearchDoc = () =>
process.env.NODE_ENV === 'production'
? fetch(`${baseUrl}${pluginData.fileNames.searchDoc}`).then((content) => content.json())
: Promise.resolve([]);
const getLunrIndex = () =>
process.env.NODE_ENV === 'production'
? fetch(`${baseUrl}${pluginData.fileNames.lunrIndex}`).then((content) => content.json())
: Promise.resolve([]);
const loadAlgolia = () => {
if (!initialized.current) {
Promise.all([getSearchDoc(), getLunrIndex(), import('./DocSearch'), import('./algolia.css')]).then(
([searchDocs, searchIndex, { default: DocSearch }]) => {
if (searchDocs.length === 0) {
return;
}
initAlgolia(searchDocs, searchIndex, DocSearch);
setIndexReady(true);
},
);
initialized.current = true;
}
};
const toggleSearchIconClick = useCallback(
(e) => {
if (!searchBarRef.current.contains(e.target)) {
searchBarRef.current.focus();
}
props.handleSearchBarToggle && props.handleSearchBarToggle(!props.isSearchBarExpanded);
},
[props.isSearchBarExpanded],
);
if (isBrowser) {
loadAlgolia();
}
return (
<div className="navbar__search" key="search-box">
<span
aria-label="expand searchbar"
role="button"
className={classnames('search-icon', {
'search-icon-hidden': props.isSearchBarExpanded,
})}
onClick={toggleSearchIconClick}
onKeyDown={toggleSearchIconClick}
tabIndex={0}
/>
<input
id="search_input_react"
type="search"
placeholder={indexReady ? 'Search' : 'Loading...'}
aria-label="Search"
className={classnames(
'navbar__search-input',
{ 'search-bar-expanded': props.isSearchBarExpanded },
{ 'search-bar': !props.isSearchBarExpanded },
)}
onClick={loadAlgolia}
onMouseOver={loadAlgolia}
onFocus={toggleSearchIconClick}
onBlur={toggleSearchIconClick}
ref={searchBarRef}
disabled={!indexReady}
/>
</div>
);
};
export default Search;
+161
View File
@@ -0,0 +1,161 @@
import lunr from '@generated/lunr.client';
lunr.tokenizer.separator = /[\s\-/]+/;
class LunrSearchAdapter {
constructor(searchDocs, searchIndex, baseUrl = '/') {
this.searchDocs = searchDocs;
this.lunrIndex = lunr.Index.load(searchIndex);
this.baseUrl = baseUrl;
}
getLunrResult(input) {
return this.lunrIndex.query(function (query) {
const tokens = lunr.tokenizer(input);
query.term(tokens, {
boost: 10,
});
query.term(tokens, {
wildcard: lunr.Query.wildcard.TRAILING,
});
});
}
getHit(doc, formattedTitle, formattedContent) {
return {
hierarchy: {
lvl0: doc.pageTitle || doc.title,
lvl1: doc.type === 0 ? null : doc.title,
},
url: doc.url,
_snippetResult: formattedContent
? {
content: {
value: formattedContent,
matchLevel: 'full',
},
}
: null,
_highlightResult: {
hierarchy: {
lvl0: {
value: doc.type === 0 ? formattedTitle || doc.title : doc.pageTitle,
},
lvl1:
doc.type === 0
? null
: {
value: formattedTitle || doc.title,
},
},
},
};
}
getTitleHit(doc, position, length) {
const start = position[0];
const end = position[0] + length;
let formattedTitle =
doc.title.substring(0, start) +
'<span class="algolia-docsearch-suggestion--highlight">' +
doc.title.substring(start, end) +
'</span>' +
doc.title.substring(end, doc.title.length);
return this.getHit(doc, formattedTitle);
}
getKeywordHit(doc, position, length) {
const start = position[0];
const end = position[0] + length;
let formattedTitle =
doc.title +
'<br /><i>Keywords: ' +
doc.keywords.substring(0, start) +
'<span class="algolia-docsearch-suggestion--highlight">' +
doc.keywords.substring(start, end) +
'</span>' +
doc.keywords.substring(end, doc.keywords.length) +
'</i>';
return this.getHit(doc, formattedTitle);
}
getContentHit(doc, position) {
const start = position[0];
const end = position[0] + position[1];
let previewStart = start;
let previewEnd = end;
let ellipsesBefore = true;
let ellipsesAfter = true;
for (let k = 0; k < 3; k++) {
const nextSpace = doc.content.lastIndexOf(' ', previewStart - 2);
const nextDot = doc.content.lastIndexOf('.', previewStart - 2);
if (nextDot > 0 && nextDot > nextSpace) {
previewStart = nextDot + 1;
ellipsesBefore = false;
break;
}
if (nextSpace < 0) {
previewStart = 0;
ellipsesBefore = false;
break;
}
previewStart = nextSpace + 1;
}
for (let k = 0; k < 10; k++) {
const nextSpace = doc.content.indexOf(' ', previewEnd + 1);
const nextDot = doc.content.indexOf('.', previewEnd + 1);
if (nextDot > 0 && nextDot < nextSpace) {
previewEnd = nextDot;
ellipsesAfter = false;
break;
}
if (nextSpace < 0) {
previewEnd = doc.content.length;
ellipsesAfter = false;
break;
}
previewEnd = nextSpace;
}
let preview = doc.content.substring(previewStart, start);
if (ellipsesBefore) {
preview = '... ' + preview;
}
preview += '<span class="algolia-docsearch-suggestion--highlight">' + doc.content.substring(start, end) + '</span>';
preview += doc.content.substring(end, previewEnd);
if (ellipsesAfter) {
preview += ' ...';
}
return this.getHit(doc, null, preview);
}
search(input) {
return new Promise((resolve, rej) => {
const results = this.getLunrResult(input);
const hits = [];
results.length > 5 && (results.length = 5);
this.titleHitsRes = [];
this.contentHitsRes = [];
results.forEach((result) => {
const doc = this.searchDocs[result.ref];
const { metadata } = result.matchData;
for (let i in metadata) {
if (metadata[i].title) {
if (!this.titleHitsRes.includes(result.ref)) {
const position = metadata[i].title.position[0];
hits.push(this.getTitleHit(doc, position, input.length));
this.titleHitsRes.push(result.ref);
}
} else if (metadata[i].content) {
const position = metadata[i].content.position[0];
hits.push(this.getContentHit(doc, position));
} else if (metadata[i].keywords) {
const position = metadata[i].keywords.position[0];
hits.push(this.getKeywordHit(doc, position, input.length));
this.titleHitsRes.push(result.ref);
}
}
});
hits.length > 5 && (hits.length = 5);
resolve(hits);
});
}
}
export default LunrSearchAdapter;
+33
View File
@@ -0,0 +1,33 @@
.search-icon {
background-image: var(--ifm-navbar-search-input-icon);
height: auto;
width: 24px;
cursor: pointer;
padding: 8px;
line-height: 32px;
background-repeat: no-repeat;
background-position: center;
display: none;
}
.search-icon-hidden {
visibility: hidden;
}
@media (max-width: 360px) {
.search-bar {
width: 0 !important;
background: none !important;
padding: 0 !important;
transition: none !important;
}
.search-bar-expanded {
width: 9rem !important;
}
.search-icon {
display: inline;
vertical-align: sub;
}
}
+112
View File
@@ -0,0 +1,112 @@
const prefix = 'algolia-docsearch';
const suggestionPrefix = `${prefix}-suggestion`;
const footerPrefix = `${prefix}-footer`;
const templates = {
suggestion: `
<a class="${suggestionPrefix}
{{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
{{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
"
aria-label="Link to the result"
href="{{{url}}}"
>
<div class="${suggestionPrefix}--category-header">
<span class="${suggestionPrefix}--category-header-lvl0">{{{category}}}</span>
</div>
<div class="${suggestionPrefix}--wrapper">
<div class="${suggestionPrefix}--subcategory-column">
<span class="${suggestionPrefix}--subcategory-column-text">{{{subcategory}}}</span>
</div>
{{#isTextOrSubcategoryNonEmpty}}
<div class="${suggestionPrefix}--content">
<div class="${suggestionPrefix}--subcategory-inline">{{{subcategory}}}</div>
<div class="${suggestionPrefix}--title">{{{title}}}</div>
{{#text}}<div class="${suggestionPrefix}--text">{{{text}}}</div>{{/text}}
</div>
{{/isTextOrSubcategoryNonEmpty}}
</div>
</a>
`,
suggestionSimple: `
<div class="${suggestionPrefix}
{{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
{{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
suggestion-layout-simple
">
<div class="${suggestionPrefix}--category-header">
{{^isLvl0}}
<span class="${suggestionPrefix}--category-header-lvl0 ${suggestionPrefix}--category-header-item">{{{category}}}</span>
{{^isLvl1}}
{{^isLvl1EmptyOrDuplicate}}
<span class="${suggestionPrefix}--category-header-lvl1 ${suggestionPrefix}--category-header-item">
{{{subcategory}}}
</span>
{{/isLvl1EmptyOrDuplicate}}
{{/isLvl1}}
{{/isLvl0}}
<div class="${suggestionPrefix}--title ${suggestionPrefix}--category-header-item">
{{#isLvl2}}
{{{title}}}
{{/isLvl2}}
{{#isLvl1}}
{{{subcategory}}}
{{/isLvl1}}
{{#isLvl0}}
{{{category}}}
{{/isLvl0}}
</div>
</div>
<div class="${suggestionPrefix}--wrapper">
{{#text}}
<div class="${suggestionPrefix}--content">
<div class="${suggestionPrefix}--text">{{{text}}}</div>
</div>
{{/text}}
</div>
</div>
`,
footer: `
<div class="${footerPrefix}">
</div>
`,
empty: `
<div class="${suggestionPrefix}">
<div class="${suggestionPrefix}--wrapper">
<div class="${suggestionPrefix}--content ${suggestionPrefix}--no-results">
<div class="${suggestionPrefix}--title">
<div class="${suggestionPrefix}--text">
No results found for query <b>"{{query}}"</b>
</div>
</div>
</div>
</div>
</div>
`,
searchBox: `
<form novalidate="novalidate" onsubmit="return false;" class="searchbox">
<div role="search" class="searchbox__wrapper">
<input id="docsearch" type="search" name="search" placeholder="Search the docs" autocomplete="off" required="required" class="searchbox__input"/>
<button type="submit" title="Submit your search query." class="searchbox__submit" >
<svg width=12 height=12 role="img" aria-label="Search">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-search-13"></use>
</svg>
</button>
<button type="reset" title="Clear the search query." class="searchbox__reset hide">
<svg width=12 height=12 role="img" aria-label="Reset">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-clear-3"></use>
</svg>
</button>
</div>
</form>
<div class="svg-icons" style="height: 0; width: 0; position: absolute; visibility: hidden">
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="sbx-icon-clear-3" viewBox="0 0 40 40"><path d="M16.228 20L1.886 5.657 0 3.772 3.772 0l1.885 1.886L20 16.228 34.343 1.886 36.228 0 40 3.772l-1.886 1.885L23.772 20l14.342 14.343L40 36.228 36.228 40l-1.885-1.886L20 23.772 5.657 38.114 3.772 40 0 36.228l1.886-1.885L16.228 20z" fill-rule="evenodd"></symbol>
<symbol id="sbx-icon-search-13" viewBox="0 0 40 40"><path d="M26.806 29.012a16.312 16.312 0 0 1-10.427 3.746C7.332 32.758 0 25.425 0 16.378 0 7.334 7.333 0 16.38 0c9.045 0 16.378 7.333 16.378 16.38 0 3.96-1.406 7.593-3.746 10.426L39.547 37.34c.607.608.61 1.59-.004 2.203a1.56 1.56 0 0 1-2.202.004L26.807 29.012zm-10.427.627c7.322 0 13.26-5.938 13.26-13.26 0-7.324-5.938-13.26-13.26-13.26-7.324 0-13.26 5.936-13.26 13.26 0 7.322 5.936 13.26 13.26 13.26z" fill-rule="evenodd"></symbol>
</svg>
</div>
`,
};
export default templates;
+266
View File
@@ -0,0 +1,266 @@
import $ from 'autocomplete.js/zepto';
const utils = {
/*
* Move the content of an object key one level higher.
* eg.
* {
* name: 'My name',
* hierarchy: {
* lvl0: 'Foo',
* lvl1: 'Bar'
* }
* }
* Will be converted to
* {
* name: 'My name',
* lvl0: 'Foo',
* lvl1: 'Bar'
* }
* @param {Object} object Main object
* @param {String} property Main object key to move up
* @return {Object}
* @throws Error when key is not an attribute of Object or is not an object itself
*/
mergeKeyWithParent(object, property) {
if (object[property] === undefined) {
return object;
}
if (typeof object[property] !== 'object') {
return object;
}
const newObject = $.extend({}, object, object[property]);
delete newObject[property];
return newObject;
},
/*
* Group all objects of a collection by the value of the specified attribute
* If the attribute is a string, use the lowercase form.
*
* eg.
* groupBy([
* {name: 'Tim', category: 'dev'},
* {name: 'Vincent', category: 'dev'},
* {name: 'Ben', category: 'sales'},
* {name: 'Jeremy', category: 'sales'},
* {name: 'AlexS', category: 'dev'},
* {name: 'AlexK', category: 'sales'}
* ], 'category');
* =>
* {
* 'devs': [
* {name: 'Tim', category: 'dev'},
* {name: 'Vincent', category: 'dev'},
* {name: 'AlexS', category: 'dev'}
* ],
* 'sales': [
* {name: 'Ben', category: 'sales'},
* {name: 'Jeremy', category: 'sales'},
* {name: 'AlexK', category: 'sales'}
* ]
* }
* @param {array} collection Array of objects to group
* @param {String} property The attribute on which apply the grouping
* @return {array}
* @throws Error when one of the element does not have the specified property
*/
groupBy(collection, property) {
const newCollection = {};
$.each(collection, (index, item) => {
if (item[property] === undefined) {
throw new Error(`[groupBy]: Object has no key ${property}`);
}
let key = item[property];
if (typeof key === 'string') {
key = key.toLowerCase();
}
// fix #171 the given data type of docsearch hits might be conflict with the properties of the native Object,
// such as the constructor, so we need to do this check.
if (!Object.prototype.hasOwnProperty.call(newCollection, key)) {
newCollection[key] = [];
}
newCollection[key].push(item);
});
return newCollection;
},
/*
* Return an array of all the values of the specified object
* eg.
* values({
* foo: 42,
* bar: true,
* baz: 'yep'
* })
* =>
* [42, true, yep]
* @param {object} object Object to extract values from
* @return {array}
*/
values(object) {
return Object.keys(object).map((key) => object[key]);
},
/*
* Flattens an array
* eg.
* flatten([1, 2, [3, 4], [5, 6]])
* =>
* [1, 2, 3, 4, 5, 6]
* @param {array} array Array to flatten
* @return {array}
*/
flatten(array) {
const results = [];
array.forEach((value) => {
if (!Array.isArray(value)) {
results.push(value);
return;
}
value.forEach((subvalue) => {
results.push(subvalue);
});
});
return results;
},
/*
* Flatten all values of an object into an array, marking each first element of
* each group with a specific flag
* eg.
* flattenAndFlagFirst({
* 'devs': [
* {name: 'Tim', category: 'dev'},
* {name: 'Vincent', category: 'dev'},
* {name: 'AlexS', category: 'dev'}
* ],
* 'sales': [
* {name: 'Ben', category: 'sales'},
* {name: 'Jeremy', category: 'sales'},
* {name: 'AlexK', category: 'sales'}
* ]
* , 'isTop');
* =>
* [
* {name: 'Tim', category: 'dev', isTop: true},
* {name: 'Vincent', category: 'dev', isTop: false},
* {name: 'AlexS', category: 'dev', isTop: false},
* {name: 'Ben', category: 'sales', isTop: true},
* {name: 'Jeremy', category: 'sales', isTop: false},
* {name: 'AlexK', category: 'sales', isTop: false}
* ]
* @param {object} object Object to flatten
* @param {string} flag Flag to set to true on first element of each group
* @return {array}
*/
flattenAndFlagFirst(object, flag) {
const values = this.values(object).map((collection) =>
collection.map((item, index) => {
// eslint-disable-next-line no-param-reassign
item[flag] = index === 0;
return item;
}),
);
return this.flatten(values);
},
/*
* Removes all empty strings, null, false and undefined elements array
* eg.
* compact([42, false, null, undefined, '', [], 'foo']);
* =>
* [42, [], 'foo']
* @param {array} array Array to compact
* @return {array}
*/
compact(array) {
const results = [];
array.forEach((value) => {
if (!value) {
return;
}
results.push(value);
});
return results;
},
/*
* Returns the highlighted value of the specified key in the specified object.
* If no highlighted value is available, will return the key value directly
* eg.
* getHighlightedValue({
* _highlightResult: {
* text: {
* value: '<mark>foo</mark>'
* }
* },
* text: 'foo'
* }, 'text');
* =>
* '<mark>foo</mark>'
* @param {object} object Hit object returned by the Algolia API
* @param {string} property Object key to look for
* @return {string}
**/
getHighlightedValue(object, property) {
if (
object._highlightResult &&
object._highlightResult.hierarchy_camel &&
object._highlightResult.hierarchy_camel[property] &&
object._highlightResult.hierarchy_camel[property].matchLevel &&
object._highlightResult.hierarchy_camel[property].matchLevel !== 'none' &&
object._highlightResult.hierarchy_camel[property].value
) {
return object._highlightResult.hierarchy_camel[property].value;
}
if (
object._highlightResult &&
object._highlightResult &&
object._highlightResult[property] &&
object._highlightResult[property].value
) {
return object._highlightResult[property].value;
}
return object[property];
},
/*
* Returns the snippeted value of the specified key in the specified object.
* If no highlighted value is available, will return the key value directly.
* Will add starting and ending ellipsis () if we detect that a sentence is
* incomplete
* eg.
* getSnippetedValue({
* _snippetResult: {
* text: {
* value: '<mark>This is an unfinished sentence</mark>'
* }
* },
* text: 'This is an unfinished sentence'
* }, 'text');
* =>
* '<mark>This is an unfinished sentence</mark>…'
* @param {object} object Hit object returned by the Algolia API
* @param {string} property Object key to look for
* @return {string}
**/
getSnippetedValue(object, property) {
if (!object._snippetResult || !object._snippetResult[property] || !object._snippetResult[property].value) {
return object[property];
}
let snippet = object._snippetResult[property].value;
if (snippet[0] !== snippet[0].toUpperCase()) {
snippet = `${snippet}`;
}
if (['.', '!', '?'].indexOf(snippet[snippet.length - 1]) === -1) {
snippet = `${snippet}`;
}
return snippet;
},
/*
* Deep clone an object.
* Note: This will not clone functions and dates
* @param {object} object Object to clone
* @return {object}
*/
deepClone(object) {
return JSON.parse(JSON.stringify(object));
},
};
export default utils;
-1
View File
@@ -23,4 +23,3 @@
/docs/features/storage-template /docs/administration/storage-template 301 /docs/features/storage-template /docs/administration/storage-template 301
/docs/features/user-management /docs/administration/user-management 301 /docs/features/user-management /docs/administration/user-management 301
/docs/developer/contributing /docs/developer/pr-checklist 301 /docs/developer/contributing /docs/developer/pr-checklist 301
/docs/guides/machine-learning /docs/guides/remote-machine-learning 301
+3 -4
View File
@@ -23,8 +23,7 @@ download_dot_env_file() {
} }
replace_env_value() { replace_env_value() {
KERNEL="$(uname -s | tr '[:upper:]' '[:lower:]')" if [[ "$OSTYPE" == "darwin"* ]]; then
if [ "$KERNEL" = "darwin" ]; then
sed -i '' "s|$1=.*|$1=$2|" ./.env sed -i '' "s|$1=.*|$1=$2|" ./.env
else else
sed -i "s|$1=.*|$1=$2|" ./.env sed -i "s|$1=.*|$1=$2|" ./.env
@@ -40,9 +39,9 @@ populate_upload_location() {
start_docker_compose() { start_docker_compose() {
echo "Starting Immich's docker containers" echo "Starting Immich's docker containers"
if docker compose > /dev/null 2>&1; then if docker compose &>/dev/null; then
docker_bin="docker compose" docker_bin="docker compose"
elif docker-compose > /dev/null 2>&1; then elif docker-compose &>/dev/null; then
docker_bin="docker-compose" docker_bin="docker-compose"
else else
echo 'Cannot find `docker compose` or `docker-compose`.' echo 'Cannot find `docker compose` or `docker-compose`.'
+4 -25
View File
@@ -1,10 +1,10 @@
import concurrent.futures
import logging import logging
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from socket import socket from socket import socket
import starlette
from gunicorn.arbiter import Arbiter from gunicorn.arbiter import Arbiter
from pydantic import BaseSettings from pydantic import BaseSettings
from rich.console import Console from rich.console import Console
@@ -70,36 +70,15 @@ LOG_LEVELS: dict[str, int] = {
settings = Settings() settings = Settings()
log_settings = LogSettings() log_settings = LogSettings()
LOG_LEVEL = LOG_LEVELS.get(log_settings.log_level.lower(), logging.INFO)
class CustomRichHandler(RichHandler): class CustomRichHandler(RichHandler):
def __init__(self) -> None: def __init__(self) -> None:
console = Console(color_system="standard", no_color=log_settings.no_color) console = Console(color_system="standard", no_color=log_settings.no_color)
self.excluded = ["uvicorn", "starlette", "fastapi"] super().__init__(show_path=False, omit_repeated_times=False, console=console, tracebacks_suppress=[starlette])
super().__init__(
show_path=False,
omit_repeated_times=False,
console=console,
rich_tracebacks=True,
tracebacks_suppress=[*self.excluded, concurrent.futures],
tracebacks_show_locals=LOG_LEVEL == logging.DEBUG,
)
# hack to exclude certain modules from rich tracebacks
def emit(self, record: logging.LogRecord) -> None:
if record.exc_info is not None:
tb = record.exc_info[2]
while tb is not None:
if any(excluded in tb.tb_frame.f_code.co_filename for excluded in self.excluded):
tb.tb_frame.f_locals["_rich_traceback_omit"] = True
tb = tb.tb_next
return super().emit(record)
log = logging.getLogger("ml.log") log = logging.getLogger("gunicorn.access")
log.setLevel(LOG_LEVEL) log.setLevel(LOG_LEVELS.get(log_settings.log_level.lower(), logging.INFO))
# patches this issue https://github.com/encode/uvicorn/discussions/1803 # patches this issue https://github.com/encode/uvicorn/discussions/1803
+13 -40
View File
@@ -14,7 +14,7 @@ import ann.ann
from app.models.constants import SUPPORTED_PROVIDERS from app.models.constants import SUPPORTED_PROVIDERS
from ..config import get_cache_dir, get_hf_model_name, log, settings from ..config import get_cache_dir, get_hf_model_name, log, settings
from ..schemas import ModelRuntime, ModelType from ..schemas import ModelType
from .ann import AnnSession from .ann import AnnSession
@@ -28,7 +28,6 @@ class InferenceModel(ABC):
providers: list[str] | None = None, providers: list[str] | None = None,
provider_options: list[dict[str, Any]] | None = None, provider_options: list[dict[str, Any]] | None = None,
sess_options: ort.SessionOptions | None = None, sess_options: ort.SessionOptions | None = None,
preferred_runtime: ModelRuntime | None = None,
**model_kwargs: Any, **model_kwargs: Any,
) -> None: ) -> None:
self.loaded = False self.loaded = False
@@ -37,7 +36,6 @@ class InferenceModel(ABC):
self.providers = providers if providers is not None else self.providers_default self.providers = providers if providers is not None else self.providers_default
self.provider_options = provider_options if provider_options is not None else self.provider_options_default self.provider_options = provider_options if provider_options is not None else self.provider_options_default
self.sess_options = sess_options if sess_options is not None else self.sess_options_default self.sess_options = sess_options if sess_options is not None else self.sess_options_default
self.preferred_runtime = preferred_runtime if preferred_runtime is not None else self.preferred_runtime_default
def download(self) -> None: def download(self) -> None:
if not self.cached: if not self.cached:
@@ -68,13 +66,11 @@ class InferenceModel(ABC):
pass pass
def _download(self) -> None: def _download(self) -> None:
ignore_patterns = [] if self.preferred_runtime == ModelRuntime.ARMNN else ["*.armnn"]
snapshot_download( snapshot_download(
get_hf_model_name(self.model_name), get_hf_model_name(self.model_name),
cache_dir=self.cache_dir, cache_dir=self.cache_dir,
local_dir=self.cache_dir, local_dir=self.cache_dir,
local_dir_use_symlinks=False, local_dir_use_symlinks=False,
ignore_patterns=ignore_patterns,
) )
@abstractmethod @abstractmethod
@@ -104,28 +100,18 @@ class InferenceModel(ABC):
self.cache_dir.mkdir(parents=True, exist_ok=True) self.cache_dir.mkdir(parents=True, exist_ok=True)
def _make_session(self, model_path: Path) -> AnnSession | ort.InferenceSession: def _make_session(self, model_path: Path) -> AnnSession | ort.InferenceSession:
if not model_path.is_file(): armnn_path = model_path.with_suffix(".armnn")
onnx_path = model_path.with_suffix(".onnx") if settings.ann and ann.ann.is_available and armnn_path.is_file():
if not onnx_path.is_file(): session = AnnSession(armnn_path)
raise ValueError(f"Model path '{model_path}' does not exist") elif model_path.is_file():
session = ort.InferenceSession(
log.warning( model_path.as_posix(),
f"Could not find model path '{model_path}'. " f"Falling back to ONNX model path '{onnx_path}' instead.", sess_options=self.sess_options,
providers=self.providers,
provider_options=self.provider_options,
) )
model_path = onnx_path else:
raise ValueError(f"the file model_path='{model_path}' does not exist")
match model_path.suffix:
case ".armnn":
session = AnnSession(model_path)
case ".onnx":
session = ort.InferenceSession(
model_path.as_posix(),
sess_options=self.sess_options,
providers=self.providers,
provider_options=self.provider_options,
)
case _:
raise ValueError(f"Unsupported model file type: {model_path.suffix}")
return session return session
@property @property
@@ -146,7 +132,7 @@ class InferenceModel(ABC):
@property @property
def cached(self) -> bool: def cached(self) -> bool:
return self.cache_dir.is_dir() and any(self.cache_dir.iterdir()) return self.cache_dir.exists() and any(self.cache_dir.iterdir())
@property @property
def providers(self) -> list[str]: def providers(self) -> list[str]:
@@ -229,19 +215,6 @@ class InferenceModel(ABC):
return sess_options return sess_options
@property
def preferred_runtime(self) -> ModelRuntime:
return self._preferred_runtime
@preferred_runtime.setter
def preferred_runtime(self, preferred_runtime: ModelRuntime) -> None:
log.debug(f"Setting preferred runtime to {preferred_runtime}")
self._preferred_runtime = preferred_runtime
@property
def preferred_runtime_default(self) -> ModelRuntime:
return ModelRuntime.ARMNN if ann.ann.is_available and settings.ann else ModelRuntime.ONNX
# HF deep copies configs, so we need to make session options picklable # HF deep copies configs, so we need to make session options picklable
class PicklableSessionOptions(ort.SessionOptions): # type: ignore[misc] class PicklableSessionOptions(ort.SessionOptions): # type: ignore[misc]
+7 -7
View File
@@ -81,11 +81,11 @@ class BaseCLIPEncoder(InferenceModel):
@property @property
def textual_path(self) -> Path: def textual_path(self) -> Path:
return self.textual_dir / f"model.{self.preferred_runtime}" return self.textual_dir / "model.onnx"
@property @property
def visual_path(self) -> Path: def visual_path(self) -> Path:
return self.visual_dir / f"model.{self.preferred_runtime}" return self.visual_dir / "model.onnx"
@property @property
def tokenizer_file_path(self) -> Path: def tokenizer_file_path(self) -> Path:
@@ -144,11 +144,11 @@ class OpenCLIPEncoder(BaseCLIPEncoder):
def _load(self) -> None: def _load(self) -> None:
super()._load() super()._load()
text_cfg: dict[str, Any] = self.model_cfg["text_cfg"]
context_length: int = text_cfg.get("context_length", 77)
pad_token: int = self.tokenizer_cfg["pad_token"]
size: list[int] | int = self.preprocess_cfg["size"] context_length = self.model_cfg["text_cfg"]["context_length"]
pad_token = self.tokenizer_cfg["pad_token"]
size = self.preprocess_cfg["size"]
self.size = size[0] if isinstance(size, list) else size self.size = size[0] if isinstance(size, list) else size
self.resampling = get_pil_resampling(self.preprocess_cfg["interpolation"]) self.resampling = get_pil_resampling(self.preprocess_cfg["interpolation"])
@@ -157,7 +157,7 @@ class OpenCLIPEncoder(BaseCLIPEncoder):
log.debug(f"Loading tokenizer for CLIP model '{self.model_name}'") log.debug(f"Loading tokenizer for CLIP model '{self.model_name}'")
self.tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix()) self.tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix())
pad_id: int = self.tokenizer.token_to_id(pad_token) pad_id = self.tokenizer.token_to_id(pad_token)
self.tokenizer.enable_padding(length=context_length, pad_token=pad_token, pad_id=pad_id) self.tokenizer.enable_padding(length=context_length, pad_token=pad_token, pad_id=pad_id)
self.tokenizer.enable_truncation(max_length=context_length) self.tokenizer.enable_truncation(max_length=context_length)
log.debug(f"Loaded tokenizer for CLIP model '{self.model_name}'") log.debug(f"Loaded tokenizer for CLIP model '{self.model_name}'")
+3 -3
View File
@@ -29,9 +29,6 @@ _OPENCLIP_MODELS = {
"ViT-L-14-quickgelu__dfn2b", "ViT-L-14-quickgelu__dfn2b",
"ViT-H-14-quickgelu__dfn5b", "ViT-H-14-quickgelu__dfn5b",
"ViT-H-14-378-quickgelu__dfn5b", "ViT-H-14-378-quickgelu__dfn5b",
"XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k",
"nllb-clip-base-siglip__v1",
"nllb-clip-large-siglip__v1",
} }
@@ -40,6 +37,9 @@ _MCLIP_MODELS = {
"XLM-Roberta-Large-Vit-B-32", "XLM-Roberta-Large-Vit-B-32",
"XLM-Roberta-Large-Vit-B-16Plus", "XLM-Roberta-Large-Vit-B-16Plus",
"XLM-Roberta-Large-Vit-L-14", "XLM-Roberta-Large-Vit-L-14",
"XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k",
"nllb-clip-base-siglip__v1",
"nllb-clip-large-siglip__v1",
} }
@@ -77,11 +77,11 @@ class FaceRecognizer(InferenceModel):
@property @property
def det_file(self) -> Path: def det_file(self) -> Path:
return self.cache_dir / "detection" / f"model.{self.preferred_runtime}" return self.cache_dir / "detection" / "model.onnx"
@property @property
def rec_file(self) -> Path: def rec_file(self) -> Path:
return self.cache_dir / "recognition" / f"model.{self.preferred_runtime}" return self.cache_dir / "recognition" / "model.onnx"
def configure(self, **model_kwargs: Any) -> None: def configure(self, **model_kwargs: Any) -> None:
self.det_model.det_thresh = model_kwargs.pop("minScore", self.det_model.det_thresh) self.det_model.det_thresh = model_kwargs.pop("minScore", self.det_model.det_thresh)
+1 -13
View File
@@ -6,13 +6,6 @@ import numpy.typing as npt
from pydantic import BaseModel from pydantic import BaseModel
class StrEnum(str, Enum):
value: str
def __str__(self) -> str:
return self.value
class TextResponse(BaseModel): class TextResponse(BaseModel):
__root__: str __root__: str
@@ -28,16 +21,11 @@ class BoundingBox(TypedDict):
y2: int y2: int
class ModelType(StrEnum): class ModelType(str, Enum):
CLIP = "clip" CLIP = "clip"
FACIAL_RECOGNITION = "facial-recognition" FACIAL_RECOGNITION = "facial-recognition"
class ModelRuntime(StrEnum):
ONNX = "onnx"
ARMNN = "armnn"
class HasProfiling(Protocol): class HasProfiling(Protocol):
profiling: dict[str, float] profiling: dict[str, float]
+21 -78
View File
@@ -18,7 +18,7 @@ from .models.base import InferenceModel, PicklableSessionOptions
from .models.cache import ModelCache from .models.cache import ModelCache
from .models.clip import OpenCLIPEncoder from .models.clip import OpenCLIPEncoder
from .models.facial_recognition import FaceRecognizer from .models.facial_recognition import FaceRecognizer
from .schemas import ModelRuntime, ModelType from .schemas import ModelType
class TestBase: class TestBase:
@@ -127,30 +127,6 @@ class TestBase:
assert encoder.cache_dir == cache_dir assert encoder.cache_dir == cache_dir
def test_sets_default_preferred_runtime(self, mocker: MockerFixture) -> None:
mocker.patch.object(settings, "ann", True)
mocker.patch("ann.ann.is_available", False)
encoder = OpenCLIPEncoder("ViT-B-32__openai")
assert encoder.preferred_runtime == ModelRuntime.ONNX
def test_sets_default_preferred_runtime_to_armnn_if_available(self, mocker: MockerFixture) -> None:
mocker.patch.object(settings, "ann", True)
mocker.patch("ann.ann.is_available", True)
encoder = OpenCLIPEncoder("ViT-B-32__openai")
assert encoder.preferred_runtime == ModelRuntime.ARMNN
def test_sets_preferred_runtime_kwarg(self, mocker: MockerFixture) -> None:
mocker.patch.object(settings, "ann", False)
mocker.patch("ann.ann.is_available", False)
encoder = OpenCLIPEncoder("ViT-B-32__openai", preferred_runtime=ModelRuntime.ARMNN)
assert encoder.preferred_runtime == ModelRuntime.ARMNN
def test_casts_cache_dir_string_to_path(self) -> None: def test_casts_cache_dir_string_to_path(self) -> None:
cache_dir = "/test_cache" cache_dir = "/test_cache"
encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir=cache_dir) encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir=cache_dir)
@@ -219,79 +195,46 @@ class TestBase:
warning.assert_called_once() warning.assert_called_once()
def test_make_session_return_ann_if_available(self, mocker: MockerFixture) -> None: def test_make_session_return_ann_if_available(self, mocker: MockerFixture) -> None:
mock_model_path = mocker.Mock() mock_cache_dir = mocker.Mock()
mock_model_path.is_file.return_value = True mock_cache_dir.is_file.return_value = True
mock_model_path.suffix = ".armnn" mock_cache_dir.with_suffix.return_value = mock_cache_dir
mock_model_path.with_suffix.return_value = mock_model_path mocker.patch.object(settings, "ann", True)
mocker.patch("ann.ann.is_available", True)
mock_session = mocker.patch("app.models.base.AnnSession") mock_session = mocker.patch("app.models.base.AnnSession")
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenCLIPEncoder("ViT-B-32__openai")
encoder._make_session(mock_model_path) encoder._make_session(mock_cache_dir)
mock_session.assert_called_once() mock_session.assert_called_once()
def test_make_session_return_ort_if_available_and_ann_is_not(self, mocker: MockerFixture) -> None: def test_make_session_return_ort_if_available_and_ann_is_not(self, mocker: MockerFixture) -> None:
mock_armnn_path = mocker.Mock() mock_cache_dir = mocker.Mock()
mock_armnn_path.is_file.return_value = False mock_cache_dir.is_file.return_value = True
mock_armnn_path.suffix = ".armnn" mock_cache_dir.with_suffix.return_value = mock_cache_dir
mocker.patch.object(settings, "ann", False)
mock_onnx_path = mocker.Mock() mocker.patch("ann.ann.is_available", False)
mock_onnx_path.is_file.return_value = True mock_session = mocker.patch("app.models.base.ort.InferenceSession")
mock_onnx_path.suffix = ".onnx"
mock_armnn_path.with_suffix.return_value = mock_onnx_path
mock_ann = mocker.patch("app.models.base.AnnSession")
mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenCLIPEncoder("ViT-B-32__openai")
encoder._make_session(mock_armnn_path) encoder._make_session(mock_cache_dir)
mock_ort.assert_called_once() mock_session.assert_called_once()
mock_ann.assert_not_called()
def test_make_session_raises_exception_if_path_does_not_exist(self, mocker: MockerFixture) -> None: def test_make_session_raises_exception_if_path_does_not_exist(self, mocker: MockerFixture) -> None:
mock_model_path = mocker.Mock() mock_cache_dir = mocker.Mock()
mock_model_path.is_file.return_value = False mock_cache_dir.is_file.return_value = False
mock_model_path.suffix = ".onnx" mock_cache_dir.with_suffix.return_value = mock_cache_dir
mock_model_path.with_suffix.return_value = mock_model_path mocker.patch("ann.ann.is_available", False)
mock_ann = mocker.patch("app.models.base.AnnSession") mock_ann = mocker.patch("app.models.base.ort.InferenceSession")
mock_ort = mocker.patch("app.models.base.ort.InferenceSession") mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenCLIPEncoder("ViT-B-32__openai")
with pytest.raises(ValueError): with pytest.raises(ValueError):
encoder._make_session(mock_model_path) encoder._make_session(mock_cache_dir)
mock_ann.assert_not_called() mock_ann.assert_not_called()
mock_ort.assert_not_called() mock_ort.assert_not_called()
def test_download(self, mocker: MockerFixture) -> None:
mock_snapshot_download = mocker.patch("app.models.base.snapshot_download")
encoder = OpenCLIPEncoder("ViT-B-32__openai")
encoder.download()
mock_snapshot_download.assert_called_once_with(
"immich-app/ViT-B-32__openai",
cache_dir=encoder.cache_dir,
local_dir=encoder.cache_dir,
local_dir_use_symlinks=False,
ignore_patterns=["*.armnn"],
)
def test_download_downloads_armnn_if_preferred_runtime(self, mocker: MockerFixture) -> None:
mock_snapshot_download = mocker.patch("app.models.base.snapshot_download")
encoder = OpenCLIPEncoder("ViT-B-32__openai", preferred_runtime=ModelRuntime.ARMNN)
encoder.download()
mock_snapshot_download.assert_called_once_with(
"immich-app/ViT-B-32__openai",
cache_dir=encoder.cache_dir,
local_dir=encoder.cache_dir,
local_dir_use_symlinks=False,
ignore_patterns=[],
)
class TestCLIP: class TestCLIP:
embedding = np.random.rand(512).astype(np.float32) embedding = np.random.rand(512).astype(np.float32)
+6 -5
View File
@@ -1,15 +1,16 @@
{ {
"version": 1, "version": 1,
"disable_existing_loggers": false, "disable_existing_loggers": true,
"formatters": { "rich": { "show_path": false, "omit_repeated_times": false } },
"handlers": { "handlers": {
"console": { "console": {
"class": "app.config.CustomRichHandler" "class": "app.config.CustomRichHandler",
"formatter": "rich"
} }
}, },
"loggers": { "loggers": {
"gunicorn.error": { "gunicorn.access": { "propagate": true },
"handlers": ["console"] "gunicorn.error": { "propagate": true }
}
}, },
"root": { "handlers": ["console"] } "root": { "handlers": ["console"] }
} }
+211 -223
View File
@@ -1272,13 +1272,13 @@ socks = ["socksio (==1.*)"]
[[package]] [[package]]
name = "huggingface-hub" name = "huggingface-hub"
version = "0.20.3" version = "0.20.2"
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.20.3-py3-none-any.whl", hash = "sha256:d988ae4f00d3e307b0c80c6a05ca6dbb7edba8bba3079f74cda7d9c2e562a7b6"}, {file = "huggingface_hub-0.20.2-py3-none-any.whl", hash = "sha256:53752eda2239d30a470c307a61cf9adcf136bc77b0a734338c7d04941af560d8"},
{file = "huggingface_hub-0.20.3.tar.gz", hash = "sha256:94e7f8e074475fbc67d6a71957b678e1b4a74ff1b64a644fd6cbb83da962d05d"}, {file = "huggingface_hub-0.20.2.tar.gz", hash = "sha256:215c5fceff631030c7a3d19ba7b588921c908b3f21eef31d160ebc245b200ff6"},
] ]
[package.dependencies] [package.dependencies]
@@ -2096,61 +2096,61 @@ numpy = [
[[package]] [[package]]
name = "orjson" name = "orjson"
version = "3.9.12" version = "3.9.10"
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.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6b4e2bed7d00753c438e83b613923afdd067564ff7ed696bfe3a7b073a236e07"}, {file = "orjson-3.9.10-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c18a4da2f50050a03d1da5317388ef84a16013302a5281d6f64e4a3f406aabc4"},
{file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd1b8ec63f0bf54a50b498eedeccdca23bd7b658f81c524d18e410c203189365"}, {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5148bab4d71f58948c7c39d12b14a9005b6ab35a0bdf317a8ade9a9e4d9d0bd5"},
{file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab8add018a53665042a5ae68200f1ad14c7953fa12110d12d41166f111724656"}, {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cf7837c3b11a2dfb589f8530b3cff2bd0307ace4c301e8997e95c7468c1378e"},
{file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12756a108875526b76e505afe6d6ba34960ac6b8c5ec2f35faf73ef161e97e07"}, {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c62b6fa2961a1dcc51ebe88771be5319a93fd89bd247c9ddf732bc250507bc2b"},
{file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:890e7519c0c70296253660455f77e3a194554a3c45e42aa193cdebc76a02d82b"}, {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb3922a7a804755bbe6b5be9b312e746137a03600f488290318936c1a2d4dc"},
{file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d664880d7f016efbae97c725b243b33c2cbb4851ddc77f683fd1eec4a7894146"}, {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1234dc92d011d3554d929b6cf058ac4a24d188d97be5e04355f1b9223e98bbe9"},
{file = "orjson-3.9.12-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cfdaede0fa5b500314ec7b1249c7e30e871504a57004acd116be6acdda3b8ab3"}, {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:06ad5543217e0e46fd7ab7ea45d506c76f878b87b1b4e369006bdb01acc05a83"},
{file = "orjson-3.9.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6492ff5953011e1ba9ed1bf086835fd574bd0a3cbe252db8e15ed72a30479081"}, {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4fd72fab7bddce46c6826994ce1e7de145ae1e9e106ebb8eb9ce1393ca01444d"},
{file = "orjson-3.9.12-cp310-none-win32.whl", hash = "sha256:29bf08e2eadb2c480fdc2e2daae58f2f013dff5d3b506edd1e02963b9ce9f8a9"}, {file = "orjson-3.9.10-cp310-none-win32.whl", hash = "sha256:b5b7d4a44cc0e6ff98da5d56cde794385bdd212a86563ac321ca64d7f80c80d1"},
{file = "orjson-3.9.12-cp310-none-win_amd64.whl", hash = "sha256:0fc156fba60d6b50743337ba09f052d8afc8b64595112996d22f5fce01ab57da"}, {file = "orjson-3.9.10-cp310-none-win_amd64.whl", hash = "sha256:61804231099214e2f84998316f3238c4c2c4aaec302df12b21a64d72e2a135c7"},
{file = "orjson-3.9.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2849f88a0a12b8d94579b67486cbd8f3a49e36a4cb3d3f0ab352c596078c730c"}, {file = "orjson-3.9.10-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cff7570d492bcf4b64cc862a6e2fb77edd5e5748ad715f487628f102815165e9"},
{file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3186b18754befa660b31c649a108a915493ea69b4fc33f624ed854ad3563ac65"}, {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8bc367f725dfc5cabeed1ae079d00369900231fbb5a5280cf0736c30e2adf7"},
{file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbbf313c9fb9d4f6cf9c22ced4b6682230457741daeb3d7060c5d06c2e73884a"}, {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c812312847867b6335cfb264772f2a7e85b3b502d3a6b0586aa35e1858528ab1"},
{file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99e8cd005b3926c3db9b63d264bd05e1bf4451787cc79a048f27f5190a9a0311"}, {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edd2856611e5050004f4722922b7b1cd6268da34102667bd49d2a2b18bafb81"},
{file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59feb148392d9155f3bfed0a2a3209268e000c2c3c834fb8fe1a6af9392efcbf"}, {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:674eb520f02422546c40401f4efaf8207b5e29e420c17051cddf6c02783ff5ca"},
{file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4ae815a172a1f073b05b9e04273e3b23e608a0858c4e76f606d2d75fcabde0c"}, {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0dc4310da8b5f6415949bd5ef937e60aeb0eb6b16f95041b5e43e6200821fb"},
{file = "orjson-3.9.12-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed398f9a9d5a1bf55b6e362ffc80ac846af2122d14a8243a1e6510a4eabcb71e"}, {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e99c625b8c95d7741fe057585176b1b8783d46ed4b8932cf98ee145c4facf499"},
{file = "orjson-3.9.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d3cfb76600c5a1e6be91326b8f3b83035a370e727854a96d801c1ea08b708073"}, {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec6f18f96b47299c11203edfbdc34e1b69085070d9a3d1f302810cc23ad36bf3"},
{file = "orjson-3.9.12-cp311-none-win32.whl", hash = "sha256:a2b6f5252c92bcab3b742ddb3ac195c0fa74bed4319acd74f5d54d79ef4715dc"}, {file = "orjson-3.9.10-cp311-none-win32.whl", hash = "sha256:ce0a29c28dfb8eccd0f16219360530bc3cfdf6bf70ca384dacd36e6c650ef8e8"},
{file = "orjson-3.9.12-cp311-none-win_amd64.whl", hash = "sha256:c95488e4aa1d078ff5776b58f66bd29d628fa59adcb2047f4efd3ecb2bd41a71"}, {file = "orjson-3.9.10-cp311-none-win_amd64.whl", hash = "sha256:cf80b550092cc480a0cbd0750e8189247ff45457e5a023305f7ef1bcec811616"},
{file = "orjson-3.9.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d6ce2062c4af43b92b0221ed4f445632c6bf4213f8a7da5396a122931377acd9"}, {file = "orjson-3.9.10-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:602a8001bdf60e1a7d544be29c82560a7b49319a0b31d62586548835bbe2c862"},
{file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:950951799967558c214cd6cceb7ceceed6f81d2c3c4135ee4a2c9c69f58aa225"}, {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f295efcd47b6124b01255d1491f9e46f17ef40d3d7eabf7364099e463fb45f0f"},
{file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2dfaf71499d6fd4153f5c86eebb68e3ec1bf95851b030a4b55c7637a37bbdee4"}, {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92af0d00091e744587221e79f68d617b432425a7e59328ca4c496f774a356071"},
{file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:659a8d7279e46c97661839035a1a218b61957316bf0202674e944ac5cfe7ed83"}, {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5a02360e73e7208a872bf65a7554c9f15df5fe063dc047f79738998b0506a14"},
{file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af17fa87bccad0b7f6fd8ac8f9cbc9ee656b4552783b10b97a071337616db3e4"}, {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858379cbb08d84fe7583231077d9a36a1a20eb72f8c9076a45df8b083724ad1d"},
{file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd52dec9eddf4c8c74392f3fd52fa137b5f2e2bed1d9ae958d879de5f7d7cded"}, {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666c6fdcaac1f13eb982b649e1c311c08d7097cbda24f32612dae43648d8db8d"},
{file = "orjson-3.9.12-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:640e2b5d8e36b970202cfd0799d11a9a4ab46cf9212332cd642101ec952df7c8"}, {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3fb205ab52a2e30354640780ce4587157a9563a68c9beaf52153e1cea9aa0921"},
{file = "orjson-3.9.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:daa438bd8024e03bcea2c5a92cd719a663a58e223fba967296b6ab9992259dbf"}, {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7ec960b1b942ee3c69323b8721df2a3ce28ff40e7ca47873ae35bfafeb4555ca"},
{file = "orjson-3.9.12-cp312-none-win_amd64.whl", hash = "sha256:1bb8f657c39ecdb924d02e809f992c9aafeb1ad70127d53fb573a6a6ab59d549"}, {file = "orjson-3.9.10-cp312-none-win_amd64.whl", hash = "sha256:3e892621434392199efb54e69edfff9f699f6cc36dd9553c5bf796058b14b20d"},
{file = "orjson-3.9.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f4098c7674901402c86ba6045a551a2ee345f9f7ed54eeffc7d86d155c8427e5"}, {file = "orjson-3.9.10-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8b9ba0ccd5a7f4219e67fbbe25e6b4a46ceef783c42af7dbc1da548eb28b6531"},
{file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5586a533998267458fad3a457d6f3cdbddbcce696c916599fa8e2a10a89b24d3"}, {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e2ecd1d349e62e3960695214f40939bbfdcaeaaa62ccc638f8e651cf0970e5f"},
{file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54071b7398cd3f90e4bb61df46705ee96cb5e33e53fc0b2f47dbd9b000e238e1"}, {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f433be3b3f4c66016d5a20e5b4444ef833a1f802ced13a2d852c637f69729c1"},
{file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:67426651faa671b40443ea6f03065f9c8e22272b62fa23238b3efdacd301df31"}, {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4689270c35d4bb3102e103ac43c3f0b76b169760aff8bcf2d401a3e0e58cdb7f"},
{file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a0cd56e8ee56b203abae7d482ac0d233dbfb436bb2e2d5cbcb539fe1200a312"}, {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd176f528a8151a6efc5359b853ba3cc0e82d4cd1fab9c1300c5d957dc8f48c"},
{file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a84a0c3d4841a42e2571b1c1ead20a83e2792644c5827a606c50fc8af7ca4bee"}, {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a2ce5ea4f71681623f04e2b7dadede3c7435dfb5e5e2d1d0ec25b35530e277b"},
{file = "orjson-3.9.12-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:09d60450cda3fa6c8ed17770c3a88473a16460cd0ff2ba74ef0df663b6fd3bb8"}, {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:49f8ad582da6e8d2cf663c4ba5bf9f83cc052570a3a767487fec6af839b0e777"},
{file = "orjson-3.9.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bc82a4db9934a78ade211cf2e07161e4f068a461c1796465d10069cb50b32a80"}, {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2a11b4b1a8415f105d989876a19b173f6cdc89ca13855ccc67c18efbd7cbd1f8"},
{file = "orjson-3.9.12-cp38-none-win32.whl", hash = "sha256:61563d5d3b0019804d782137a4f32c72dc44c84e7d078b89d2d2a1adbaa47b52"}, {file = "orjson-3.9.10-cp38-none-win32.whl", hash = "sha256:a353bf1f565ed27ba71a419b2cd3db9d6151da426b61b289b6ba1422a702e643"},
{file = "orjson-3.9.12-cp38-none-win_amd64.whl", hash = "sha256:410f24309fbbaa2fab776e3212a81b96a1ec6037259359a32ea79fbccfcf76aa"}, {file = "orjson-3.9.10-cp38-none-win_amd64.whl", hash = "sha256:e28a50b5be854e18d54f75ef1bb13e1abf4bc650ab9d635e4258c58e71eb6ad5"},
{file = "orjson-3.9.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e773f251258dd82795fd5daeac081d00b97bacf1548e44e71245543374874bcf"}, {file = "orjson-3.9.10-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ee5926746232f627a3be1cc175b2cfad24d0170d520361f4ce3fa2fd83f09e1d"},
{file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b159baecfda51c840a619948c25817d37733a4d9877fea96590ef8606468b362"}, {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a73160e823151f33cdc05fe2cea557c5ef12fdf276ce29bb4f1c571c8368a60"},
{file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:975e72e81a249174840d5a8df977d067b0183ef1560a32998be340f7e195c730"}, {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c338ed69ad0b8f8f8920c13f529889fe0771abbb46550013e3c3d01e5174deef"},
{file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06e42e899dde61eb1851a9fad7f1a21b8e4be063438399b63c07839b57668f6c"}, {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5869e8e130e99687d9e4be835116c4ebd83ca92e52e55810962446d841aba8de"},
{file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c157e999e5694475a5515942aebeed6e43f7a1ed52267c1c93dcfde7d78d421"}, {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2c1e559d96a7f94a4f581e2a32d6d610df5840881a8cba8f25e446f4d792df3"},
{file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dde1bc7c035f2d03aa49dc8642d9c6c9b1a81f2470e02055e76ed8853cfae0c3"}, {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a3a3a72c9811b56adf8bcc829b010163bb2fc308877e50e9910c9357e78521"},
{file = "orjson-3.9.12-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b0e9d73cdbdad76a53a48f563447e0e1ce34bcecef4614eb4b146383e6e7d8c9"}, {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7f8fb7f5ecf4f6355683ac6881fd64b5bb2b8a60e3ccde6ff799e48791d8f864"},
{file = "orjson-3.9.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:96e44b21fe407b8ed48afbb3721f3c8c8ce17e345fbe232bd4651ace7317782d"}, {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c943b35ecdf7123b2d81d225397efddf0bce2e81db2f3ae633ead38e85cd5ade"},
{file = "orjson-3.9.12-cp39-none-win32.whl", hash = "sha256:cbd0f3555205bf2a60f8812133f2452d498dbefa14423ba90fe89f32276f7abf"}, {file = "orjson-3.9.10-cp39-none-win32.whl", hash = "sha256:fb0b361d73f6b8eeceba47cd37070b5e6c9de5beaeaa63a1cb35c7e1a73ef088"},
{file = "orjson-3.9.12-cp39-none-win_amd64.whl", hash = "sha256:03ea7ee7e992532c2f4a06edd7ee1553f0644790553a118e003e3c405add41fa"}, {file = "orjson-3.9.10-cp39-none-win_amd64.whl", hash = "sha256:b90f340cb6397ec7a854157fac03f0c82b744abdd1c0941a024c3c29d1340aff"},
{file = "orjson-3.9.12.tar.gz", hash = "sha256:da908d23a3b3243632b523344403b128722a5f45e278a8343c2bb67538dff0e4"}, {file = "orjson-3.9.10.tar.gz", hash = "sha256:9ebbdbd6a046c304b1845e96fbcc5559cd296b4dfd3ad2509e33c4d9ce07d6a1"},
] ]
[[package]] [[package]]
@@ -2368,47 +2368,47 @@ files = [
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "1.10.14" version = "1.10.13"
description = "Data validation and settings management using python type hints" description = "Data validation and settings management using python type hints"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"},
{file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"},
{file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"},
{file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"}, {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"},
{file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"},
{file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"},
{file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"},
{file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"},
{file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"},
{file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"},
{file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"}, {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"},
{file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"},
{file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"},
{file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"},
{file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"},
{file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"},
{file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"}, {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"},
{file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"},
{file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"},
{file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"},
{file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"},
{file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"},
{file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"},
{file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"}, {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"},
{file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"},
{file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"},
{file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"},
{file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"},
{file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"},
{file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"},
{file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"}, {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"},
{file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"},
{file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"},
{file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"},
{file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"},
{file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"},
] ]
[package.dependencies] [package.dependencies]
@@ -2831,28 +2831,28 @@ files = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.1.15" version = "0.1.13"
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.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e3fd36e0d48aeac672aa850045e784673449ce619afc12823ea7868fcc41d8ba"},
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9fb6b3b86450d4ec6a6732f9f60c4406061b6851c4b29f944f8c9d91c3611c7a"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b13ba5d7156daaf3fd08b6b993360a96060500aca7e307d95ecbc5bb47a69296"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ebb40442f7b531e136d334ef0851412410061e65d61ca8ce90d894a094feb22"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226b517f42d59a543d6383cfe03cccf0091e3e0ed1b856c6824be03d2a75d3b6"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0312ba1061e9b8c724e9a702d3c8621e3c6e6c2c9bd862550ab2951ac75c16"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f59bcf5217c661254bd6bc42d65a6fd1a8b80c48763cb5c2293295babd945dd"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6894b00495e00c27b6ba61af1fc666f17de6140345e5ef27dd6e08fb987259d"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1600942485c6e66119da294c6294856b5c86fd6df591ce293e4a4cc8e72989"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, {file = "ruff-0.1.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ee3febce7863e231a467f90e681d3d89210b900d49ce88723ce052c8761be8c7"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, {file = "ruff-0.1.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dcaab50e278ff497ee4d1fe69b29ca0a9a47cd954bb17963628fa417933c6eb1"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, {file = "ruff-0.1.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f57de973de4edef3ad3044d6a50c02ad9fc2dff0d88587f25f1a48e3f72edf5e"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, {file = "ruff-0.1.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a36fa90eb12208272a858475ec43ac811ac37e91ef868759770b71bdabe27b6"},
{file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, {file = "ruff-0.1.13-py3-none-win32.whl", hash = "sha256:a623349a505ff768dad6bd57087e2461be8db58305ebd5577bd0e98631f9ae69"},
{file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, {file = "ruff-0.1.13-py3-none-win_amd64.whl", hash = "sha256:f988746e3c3982bea7f824c8fa318ce7f538c4dfefec99cd09c8770bd33e6539"},
{file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, {file = "ruff-0.1.13-py3-none-win_arm64.whl", hash = "sha256:6bbbc3042075871ec17f28864808540a26f0f79a4478c357d3e3d2284e832998"},
{file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, {file = "ruff-0.1.13.tar.gz", hash = "sha256:e261f1baed6291f434ffb1d5c6bd8051d1c2a26958072d38dfbec39b3dda7352"},
] ]
[[package]] [[package]]
@@ -3091,121 +3091,109 @@ all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib"
[[package]] [[package]]
name = "tokenizers" name = "tokenizers"
version = "0.15.1" version = "0.15.0"
description = "" description = ""
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "tokenizers-0.15.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:32c9491dd1bcb33172c26b454dbd607276af959b9e78fa766e2694cafab3103c"}, {file = "tokenizers-0.15.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:cd3cd0299aaa312cd2988957598f80becd04d5a07338741eca076057a2b37d6e"},
{file = "tokenizers-0.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29a1b784b870a097e7768f8c20c2dd851e2c75dad3efdae69a79d3e7f1d614d5"}, {file = "tokenizers-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a922c492c721744ee175f15b91704be2d305569d25f0547c77cd6c9f210f9dc"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0049fbe648af04148b08cb211994ce8365ee628ce49724b56aaefd09a3007a78"}, {file = "tokenizers-0.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:331dd786d02fc38698f835fff61c99480f98b73ce75a4c65bd110c9af5e4609a"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e84b3c235219e75e24de6b71e6073cd2c8d740b14d88e4c6d131b90134e3a338"}, {file = "tokenizers-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88dd0961c437d413ab027f8b115350c121d49902cfbadf08bb8f634b15fa1814"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8cc575769ea11d074308c6d71cb10b036cdaec941562c07fc7431d956c502f0e"}, {file = "tokenizers-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6fdcc55339df7761cd52e1fbe8185d3b3963bc9e3f3545faa6c84f9e8818259a"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bf28f299c4158e6d0b5eaebddfd500c4973d947ffeaca8bcbe2e8c137dff0b"}, {file = "tokenizers-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1480b0051d8ab5408e8e4db2dc832f7082ea24aa0722c427bde2418c6f3bd07"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:506555f98361db9c74e1323a862d77dcd7d64c2058829a368bf4159d986e339f"}, {file = "tokenizers-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9855e6c258918f9cf62792d4f6ddfa6c56dccd8c8118640f867f6393ecaf8bd7"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7061b0a28ade15906f5b2ec8c48d3bdd6e24eca6b427979af34954fbe31d5cef"}, {file = "tokenizers-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de9529fe75efcd54ba8d516aa725e1851df9199f0669b665c55e90df08f5af86"},
{file = "tokenizers-0.15.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ed5e35507b7a0e2aac3285c4f5e37d4ec5cfc0e5825b862b68a0aaf2757af52"}, {file = "tokenizers-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8edcc90a36eab0705fe9121d6c77c6e42eeef25c7399864fd57dfb27173060bf"},
{file = "tokenizers-0.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c9df9247df0de6509dd751b1c086e5f124b220133b5c883bb691cb6fb3d786f"}, {file = "tokenizers-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ae17884aafb3e94f34fb7cfedc29054f5f54e142475ebf8a265a4e388fee3f8b"},
{file = "tokenizers-0.15.1-cp310-none-win32.whl", hash = "sha256:dd999af1b4848bef1b11d289f04edaf189c269d5e6afa7a95fa1058644c3f021"}, {file = "tokenizers-0.15.0-cp310-none-win32.whl", hash = "sha256:9a3241acdc9b44cff6e95c4a55b9be943ef3658f8edb3686034d353734adba05"},
{file = "tokenizers-0.15.1-cp310-none-win_amd64.whl", hash = "sha256:39d06a57f7c06940d602fad98702cf7024c4eee7f6b9fe76b9f2197d5a4cc7e2"}, {file = "tokenizers-0.15.0-cp310-none-win_amd64.whl", hash = "sha256:4b31807cb393d6ea31926b307911c89a1209d5e27629aa79553d1599c8ffdefe"},
{file = "tokenizers-0.15.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8ad034eb48bf728af06915e9294871f72fcc5254911eddec81d6df8dba1ce055"}, {file = "tokenizers-0.15.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:af7e9be8c05d30bb137b9fd20f9d99354816599e5fd3d58a4b1e28ba3b36171f"},
{file = "tokenizers-0.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea9ede7c42f8fa90f31bfc40376fd91a7d83a4aa6ad38e6076de961d48585b26"}, {file = "tokenizers-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3d7343fa562ea29661783344a2d83662db0d3d17a6fa6a403cac8e512d2d9fd"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b85d6fe1a20d903877aa0ef32ef6b96e81e0e48b71c206d6046ce16094de6970"}, {file = "tokenizers-0.15.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:32371008788aeeb0309a9244809a23e4c0259625e6b74a103700f6421373f395"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a7d44f656320137c7d643b9c7dcc1814763385de737fb98fd2643880910f597"}, {file = "tokenizers-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9db64c7c9954fbae698884c5bb089764edc549731e5f9b7fa1dd4e4d78d77f"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd244bd0793cdacf27ee65ec3db88c21f5815460e8872bbeb32b040469d6774e"}, {file = "tokenizers-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbed5944c31195514669cf6381a0d8d47f164943000d10f93d6d02f0d45c25e0"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3f4a36e371b3cb1123adac8aeeeeab207ad32f15ed686d9d71686a093bb140"}, {file = "tokenizers-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aab16c4a26d351d63e965b0c792f5da7227a37b69a6dc6d922ff70aa595b1b0c"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2921a53966afb29444da98d56a6ccbef23feb3b0c0f294b4e502370a0a64f25"}, {file = "tokenizers-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c2b60b12fdd310bf85ce5d7d3f823456b9b65eed30f5438dd7761879c495983"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f49068cf51f49c231067f1a8c9fc075ff960573f6b2a956e8e1b0154fb638ea5"}, {file = "tokenizers-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0344d6602740e44054a9e5bbe9775a5e149c4dddaff15959bb07dcce95a5a859"},
{file = "tokenizers-0.15.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0ab1a22f20eaaab832ab3b00a0709ca44a0eb04721e580277579411b622c741c"}, {file = "tokenizers-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4525f6997d81d9b6d9140088f4f5131f6627e4c960c2c87d0695ae7304233fc3"},
{file = "tokenizers-0.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:671268f24b607c4adc6fa2b5b580fd4211b9f84b16bd7f46d62f8e5be0aa7ba4"}, {file = "tokenizers-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:65975094fef8cc68919644936764efd2ce98cf1bacbe8db2687155d2b0625bee"},
{file = "tokenizers-0.15.1-cp311-none-win32.whl", hash = "sha256:a4f03e33d2bf7df39c8894032aba599bf90f6f6378e683a19d28871f09bb07fc"}, {file = "tokenizers-0.15.0-cp311-none-win32.whl", hash = "sha256:ff5d2159c5d93015f5a4542aac6c315506df31853123aa39042672031768c301"},
{file = "tokenizers-0.15.1-cp311-none-win_amd64.whl", hash = "sha256:30f689537bcc7576d8bd4daeeaa2cb8f36446ba2f13f421b173e88f2d8289c4e"}, {file = "tokenizers-0.15.0-cp311-none-win_amd64.whl", hash = "sha256:2dd681b53cf615e60a31a115a3fda3980e543d25ca183797f797a6c3600788a3"},
{file = "tokenizers-0.15.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f3a379dd0898a82ea3125e8f9c481373f73bffce6430d4315f0b6cd5547e409"}, {file = "tokenizers-0.15.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:c9cce6ee149a3d703f86877bc2a6d997e34874b2d5a2d7839e36b2273f31d3d9"},
{file = "tokenizers-0.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d870ae58bba347d38ac3fc8b1f662f51e9c95272d776dd89f30035c83ee0a4f"}, {file = "tokenizers-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a0a94bc3370e6f1cc8a07a8ae867ce13b7c1b4291432a773931a61f256d44ea"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d6d28e0143ec2e253a8a39e94bf1d24776dbe73804fa748675dbffff4a5cd6d8"}, {file = "tokenizers-0.15.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:309cfcccfc7e502cb1f1de2c9c1c94680082a65bfd3a912d5a5b2c90c677eb60"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61ae9ac9f44e2da128ee35db69489883b522f7abe033733fa54eb2de30dac23d"}, {file = "tokenizers-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8413e994dd7d875ab13009127fc85633916c71213917daf64962bafd488f15dc"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d8e322a47e29128300b3f7749a03c0ec2bce0a3dc8539ebff738d3f59e233542"}, {file = "tokenizers-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0ebf9430f901dbdc3dcb06b493ff24a3644c9f88c08e6a1d6d0ae2228b9b818"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:760334f475443bc13907b1a8e1cb0aeaf88aae489062546f9704dce6c498bfe2"}, {file = "tokenizers-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10361e9c7864b22dd791ec5126327f6c9292fb1d23481d4895780688d5e298ac"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b173753d4aca1e7d0d4cb52b5e3ffecfb0ca014e070e40391b6bb4c1d6af3f2"}, {file = "tokenizers-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:babe42635b8a604c594bdc56d205755f73414fce17ba8479d142a963a6c25cbc"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c1f13d457c8f0ab17e32e787d03470067fe8a3b4d012e7cc57cb3264529f4a"}, {file = "tokenizers-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3768829861e964c7a4556f5f23307fce6a23872c2ebf030eb9822dbbbf7e9b2a"},
{file = "tokenizers-0.15.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:425b46ceff4505f20191df54b50ac818055d9d55023d58ae32a5d895b6f15bb0"}, {file = "tokenizers-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9c91588a630adc88065e1c03ac6831e3e2112558869b9ebcb2b8afd8a14c944d"},
{file = "tokenizers-0.15.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:681ac6ba3b4fdaf868ead8971221a061f580961c386e9732ea54d46c7b72f286"}, {file = "tokenizers-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:77606994e793ca54ecf3a3619adc8a906a28ca223d9354b38df41cb8766a0ed6"},
{file = "tokenizers-0.15.1-cp312-none-win32.whl", hash = "sha256:f2272656063ccfba2044df2115095223960d80525d208e7a32f6c01c351a6f4a"}, {file = "tokenizers-0.15.0-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:6fe143939f3b596681922b2df12a591a5b010e7dcfbee2202482cd0c1c2f2459"},
{file = "tokenizers-0.15.1-cp312-none-win_amd64.whl", hash = "sha256:9abe103203b1c6a2435d248d5ff4cceebcf46771bfbc4957a98a74da6ed37674"}, {file = "tokenizers-0.15.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:b7bee0f1795e3e3561e9a557061b1539e5255b8221e3f928f58100282407e090"},
{file = "tokenizers-0.15.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2ce9ed5c8ef26b026a66110e3c7b73d93ec2d26a0b1d0ea55ddce61c0e5f446f"}, {file = "tokenizers-0.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5d37e7f4439b4c46192ab4f2ff38ab815e4420f153caa13dec9272ef14403d34"},
{file = "tokenizers-0.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:89b24d366137986c3647baac29ef902d2d5445003d11c30df52f1bd304689aeb"}, {file = "tokenizers-0.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caadf255cf7f951b38d10097836d1f3bcff4aeaaffadfdf748bab780bf5bff95"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0faebedd01b413ab777ca0ee85914ed8b031ea5762ab0ea60b707ce8b9be6842"}, {file = "tokenizers-0.15.0-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05accb9162bf711a941b1460b743d62fec61c160daf25e53c5eea52c74d77814"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbd9dfcdad4f3b95d801f768e143165165055c18e44ca79a8a26de889cd8e85"}, {file = "tokenizers-0.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26a2ef890740127cb115ee5260878f4a677e36a12831795fd7e85887c53b430b"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:97194324c12565b07e9993ca9aa813b939541185682e859fb45bb8d7d99b3193"}, {file = "tokenizers-0.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e54c5f26df14913620046b33e822cb3bcd091a332a55230c0e63cc77135e2169"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485e43e2cc159580e0d83fc919ec3a45ae279097f634b1ffe371869ffda5802c"}, {file = "tokenizers-0.15.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669b8ed653a578bcff919566631156f5da3aab84c66f3c0b11a6281e8b4731c7"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:191d084d60e3589d6420caeb3f9966168269315f8ec7fbc3883122dc9d99759d"}, {file = "tokenizers-0.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0ea480d943297df26f06f508dab6e012b07f42bf3dffdd36e70799368a5f5229"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01c28cc8d7220634a75b14c53f4fc9d1b485f99a5a29306a999c115921de2897"}, {file = "tokenizers-0.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc80a0a565ebfc7cd89de7dd581da8c2b3238addfca6280572d27d763f135f2f"},
{file = "tokenizers-0.15.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:325212027745d3f8d5d5006bb9e5409d674eb80a184f19873f4f83494e1fdd26"}, {file = "tokenizers-0.15.0-cp37-none-win32.whl", hash = "sha256:cdd945e678bbdf4517d5d8de66578a5030aeefecdb46f5320b034de9cad8d4dd"},
{file = "tokenizers-0.15.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3c5573603c36ce12dbe318bcfb490a94cad2d250f34deb2f06cb6937957bbb71"}, {file = "tokenizers-0.15.0-cp37-none-win_amd64.whl", hash = "sha256:1ab96ab7dc706e002c32b2ea211a94c1c04b4f4de48354728c3a6e22401af322"},
{file = "tokenizers-0.15.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:1441161adb6d71a15a630d5c1d8659d5ebe41b6b209586fbeea64738e58fcbb2"}, {file = "tokenizers-0.15.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:f21c9eb71c9a671e2a42f18b456a3d118e50c7f0fc4dd9fa8f4eb727fea529bf"},
{file = "tokenizers-0.15.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:382a8d0c31afcfb86571afbfefa37186df90865ce3f5b731842dab4460e53a38"}, {file = "tokenizers-0.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a5f4543a35889679fc3052086e69e81880b2a5a28ff2a52c5a604be94b77a3f"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e76959783e3f4ec73b3f3d24d4eec5aa9225f0bee565c48e77f806ed1e048f12"}, {file = "tokenizers-0.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f8aa81afec893e952bd39692b2d9ef60575ed8c86fce1fd876a06d2e73e82dca"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:401df223e5eb927c5961a0fc6b171818a2bba01fb36ef18c3e1b69b8cd80e591"}, {file = "tokenizers-0.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1574a5a4af22c3def93fe8fe4adcc90a39bf5797ed01686a4c46d1c3bc677d2f"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52606c233c759561a16e81b2290a7738c3affac7a0b1f0a16fe58dc22e04c7d"}, {file = "tokenizers-0.15.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c7982fd0ec9e9122d03b209dac48cebfea3de0479335100ef379a9a959b9a5a"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72c658bbe5a05ed8bc2ac5ad782385bfd743ffa4bc87d9b5026341e709c6f44"}, {file = "tokenizers-0.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d16b647032df2ce2c1f9097236e046ea9fedd969b25637b9d5d734d78aa53b"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25f5643a2f005c42f0737a326c6c6bdfedfdc9a994b10a1923d9c3e792e4d6a6"}, {file = "tokenizers-0.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b3cdf29e6f9653da330515dc8fa414be5a93aae79e57f8acc50d4028dd843edf"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c5b6f633999d6b42466bbfe21be2e26ad1760b6f106967a591a41d8cbca980e"}, {file = "tokenizers-0.15.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7286f3df10de840867372e3e64b99ef58c677210e3ceb653cd0e740a5c53fe78"},
{file = "tokenizers-0.15.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ceb5c9ad11a015150b545c1a11210966a45b8c3d68a942e57cf8938c578a77ca"}, {file = "tokenizers-0.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aabc83028baa5a36ce7a94e7659250f0309c47fa4a639e5c2c38e6d5ea0de564"},
{file = "tokenizers-0.15.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bedd4ce0c4872db193444c395b11c7697260ce86a635ab6d48102d76be07d324"}, {file = "tokenizers-0.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:72f78b0e0e276b1fc14a672fa73f3acca034ba8db4e782124a2996734a9ba9cf"},
{file = "tokenizers-0.15.1-cp37-none-win32.whl", hash = "sha256:cd6caef6c14f5ed6d35f0ddb78eab8ca6306d0cd9870330bccff72ad014a6f42"}, {file = "tokenizers-0.15.0-cp38-none-win32.whl", hash = "sha256:9680b0ecc26e7e42f16680c1aa62e924d58d1c2dd992707081cc10a374896ea2"},
{file = "tokenizers-0.15.1-cp37-none-win_amd64.whl", hash = "sha256:d2bd7af78f58d75a55e5df61efae164ab9200c04b76025f9cc6eeb7aff3219c2"}, {file = "tokenizers-0.15.0-cp38-none-win_amd64.whl", hash = "sha256:f17cbd88dab695911cbdd385a5a7e3709cc61dff982351f5d1b5939f074a2466"},
{file = "tokenizers-0.15.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:59b3ca6c02e0bd5704caee274978bd055de2dff2e2f39dadf536c21032dfd432"}, {file = "tokenizers-0.15.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:3661862df7382c5eb23ac4fbf7c75e69b02dc4f5784e4c5a734db406b5b24596"},
{file = "tokenizers-0.15.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:48fe21b67c22583bed71933a025fd66b1f5cfae1baefa423c3d40379b5a6e74e"}, {file = "tokenizers-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3045d191dad49647f5a5039738ecf1c77087945c7a295f7bcf051c37067e883"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3d190254c66a20fb1efbdf035e6333c5e1f1c73b1f7bfad88f9c31908ac2c2c4"}, {file = "tokenizers-0.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9fcaad9ab0801f14457d7c820d9f246b5ab590c407fc6b073819b1573097aa7"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fef90c8f5abf17d48d6635f5fd92ad258acd1d0c2d920935c8bf261782cfe7c8"}, {file = "tokenizers-0.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79f17027f24fe9485701c8dbb269b9c713954ec3bdc1e7075a66086c0c0cd3c"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fac011ef7da3357aa7eb19efeecf3d201ede9618f37ddedddc5eb809ea0963ca"}, {file = "tokenizers-0.15.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01a3aa332abc4bee7640563949fcfedca4de8f52691b3b70f2fc6ca71bfc0f4e"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:574ec5b3e71d1feda6b0ecac0e0445875729b4899806efbe2b329909ec75cb50"}, {file = "tokenizers-0.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05b83896a893cdfedad8785250daa3ba9f0504848323471524d4783d7291661e"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aca16c3c0637c051a59ea99c4253f16fbb43034fac849076a7e7913b2b9afd2d"}, {file = "tokenizers-0.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbbf2489fcf25d809731ba2744ff278dd07d9eb3f8b7482726bd6cae607073a4"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6f238fc2bbfd3e12e8529980ec1624c7e5b69d4e959edb3d902f36974f725a"}, {file = "tokenizers-0.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab806ad521a5e9de38078b7add97589c313915f6f5fec6b2f9f289d14d607bd6"},
{file = "tokenizers-0.15.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:587e11a26835b73c31867a728f32ca8a93c9ded4a6cd746516e68b9d51418431"}, {file = "tokenizers-0.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a522612d5c88a41563e3463226af64e2fa00629f65cdcc501d1995dd25d23f5"},
{file = "tokenizers-0.15.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6456e7ad397352775e2efdf68a9ec5d6524bbc4543e926eef428d36de627aed4"}, {file = "tokenizers-0.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e58a38c4e6075810bdfb861d9c005236a72a152ebc7005941cc90d1bbf16aca9"},
{file = "tokenizers-0.15.1-cp38-none-win32.whl", hash = "sha256:614f0da7dd73293214bd143e6221cafd3f7790d06b799f33a987e29d057ca658"}, {file = "tokenizers-0.15.0-cp39-none-win32.whl", hash = "sha256:b8034f1041fd2bd2b84ff9f4dc4ae2e1c3b71606820a9cd5c562ebd291a396d1"},
{file = "tokenizers-0.15.1-cp38-none-win_amd64.whl", hash = "sha256:a4fa0a20d9f69cc2bf1cfce41aa40588598e77ec1d6f56bf0eb99769969d1ede"}, {file = "tokenizers-0.15.0-cp39-none-win_amd64.whl", hash = "sha256:edde9aa964145d528d0e0dbf14f244b8a85ebf276fb76869bc02e2530fa37a96"},
{file = "tokenizers-0.15.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8d3f18a45e0cf03ce193d5900460dc2430eec4e14c786e5d79bddba7ea19034f"}, {file = "tokenizers-0.15.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:309445d10d442b7521b98083dc9f0b5df14eca69dbbfebeb98d781ee2cef5d30"},
{file = "tokenizers-0.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:38dbd6c38f88ad7d5dc5d70c764415d38fe3bcd99dc81638b572d093abc54170"}, {file = "tokenizers-0.15.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d3125a6499226d4d48efc54f7498886b94c418e93a205b673bc59364eecf0804"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:777286b1f7e52de92aa4af49fe31046cfd32885d1bbaae918fab3bba52794c33"}, {file = "tokenizers-0.15.0-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ed56ddf0d54877bb9c6d885177db79b41576e61b5ef6defeb579dcb803c04ad5"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58d4d550a3862a47dd249892d03a025e32286eb73cbd6bc887fb8fb64bc97165"}, {file = "tokenizers-0.15.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b22cd714706cc5b18992a232b023f736e539495f5cc61d2d28d176e55046f6c"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eda68ce0344f35042ae89220b40a0007f721776b727806b5c95497b35714bb7"}, {file = "tokenizers-0.15.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac2719b1e9bc8e8e7f6599b99d0a8e24f33d023eb8ef644c0366a596f0aa926"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cd33d15f7a3a784c3b665cfe807b8de3c6779e060349bd5005bb4ae5bdcb437"}, {file = "tokenizers-0.15.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:85ddae17570ec7e5bfaf51ffa78d044f444a8693e1316e1087ee6150596897ee"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a1aa370f978ac0bfb50374c3a40daa93fd56d47c0c70f0c79607fdac2ccbb42"}, {file = "tokenizers-0.15.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76f1bed992e396bf6f83e3df97b64ff47885e45e8365f8983afed8556a0bc51f"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:241482b940340fff26a2708cb9ba383a5bb8a2996d67a0ff2c4367bf4b86cc3a"}, {file = "tokenizers-0.15.0-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3bb0f4df6dce41a1c7482087b60d18c372ef4463cb99aa8195100fcd41e0fd64"},
{file = "tokenizers-0.15.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:68f30b05f46a4d9aba88489eadd021904afe90e10a7950e28370d6e71b9db021"}, {file = "tokenizers-0.15.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:22c27672c27a059a5f39ff4e49feed8c7f2e1525577c8a7e3978bd428eb5869d"},
{file = "tokenizers-0.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5a3c5d8025529670462b881b7b2527aacb6257398c9ec8e170070432c3ae3a82"}, {file = "tokenizers-0.15.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78104f5d035c9991f92831fc0efe9e64a05d4032194f2a69f67aaa05a4d75bbb"},
{file = "tokenizers-0.15.1-cp39-none-win32.whl", hash = "sha256:74d1827830f60a9d78da8f6d49a1fbea5422ce0eea42e2617877d23380a7efbc"}, {file = "tokenizers-0.15.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a40b73dc19d82c3e3ffb40abdaacca8fbc95eeb26c66b7f9f860aebc07a73998"},
{file = "tokenizers-0.15.1-cp39-none-win_amd64.whl", hash = "sha256:9ff499923e4d6876d6b6a63ea84a56805eb35e91dd89b933a7aee0c56a3838c6"}, {file = "tokenizers-0.15.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d801d1368188c74552cd779b1286e67cb9fd96f4c57a9f9a2a09b6def9e1ab37"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b3aa007a0f4408f62a8471bdaa3faccad644cbf2622639f2906b4f9b5339e8b8"}, {file = "tokenizers-0.15.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82641ffb13a4da1293fcc9f437d457647e60ed0385a9216cd135953778b3f0a1"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f3d4176fa93d8b2070db8f3c70dc21106ae6624fcaaa334be6bdd3a0251e729e"}, {file = "tokenizers-0.15.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:160f9d1810f2c18fffa94aa98bf17632f6bd2dabc67fcb01a698ca80c37d52ee"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d0e463655ef8b2064df07bd4a445ed7f76f6da3b286b4590812587d42f80e89"}, {file = "tokenizers-0.15.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:8d7d6eea831ed435fdeeb9bcd26476226401d7309d115a710c65da4088841948"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:089138fd0351b62215c462a501bd68b8df0e213edcf99ab9efd5dba7b4cb733e"}, {file = "tokenizers-0.15.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f6456bec6c557d63d8ec0023758c32f589e1889ed03c055702e84ce275488bed"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e563ac628f5175ed08e950430e2580e544b3e4b606a0995bb6b52b3a3165728"}, {file = "tokenizers-0.15.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eef39a502fad3bf104b9e1906b4fb0cee20e44e755e51df9a98f8922c3bf6d4"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:244dcc28c5fde221cb4373961b20da30097669005b122384d7f9f22752487a46"}, {file = "tokenizers-0.15.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1e4664c5b797e093c19b794bbecc19d2367e782b4a577d8b7c1821db5dc150d"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d82951d46052dddae1369e68ff799a0e6e29befa9a0b46e387ae710fd4daefb0"}, {file = "tokenizers-0.15.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ca003fb5f3995ff5cf676db6681b8ea5d54d3b30bea36af1120e78ee1a4a4cdf"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7b14296bc9059849246ceb256ffbe97f8806a9b5d707e0095c22db312f4fc014"}, {file = "tokenizers-0.15.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7f17363141eb0c53752c89e10650b85ef059a52765d0802ba9613dbd2d21d425"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0309357bb9b6c8d86cdf456053479d7112074b470651a997a058cd7ad1c4ea57"}, {file = "tokenizers-0.15.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:8a765db05581c7d7e1280170f2888cda351760d196cc059c37ea96f121125799"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083f06e9d8d01b70b67bcbcb7751b38b6005512cce95808be6bf34803534a7e7"}, {file = "tokenizers-0.15.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2a0dd641a72604486cd7302dd8f87a12c8a9b45e1755e47d2682733f097c1af5"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85288aea86ada579789447f0dcec108ebef8da4b450037eb4813d83e4da9371e"}, {file = "tokenizers-0.15.0-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a1a3c973e4dc97797fc19e9f11546c95278ffc55c4492acb742f69e035490bc"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:385e6fcb01e8de90c1d157ae2a5338b23368d0b1c4cc25088cdca90147e35d17"}, {file = "tokenizers-0.15.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4fab75642aae4e604e729d6f78e0addb9d7e7d49e28c8f4d16b24da278e5263"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:60067edfcbf7d6cd448ac47af41ec6e84377efbef7be0c06f15a7c1dd069e044"}, {file = "tokenizers-0.15.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65f80be77f6327a86d8fd35a4467adcfe6174c159b4ab52a1a8dd4c6f2d7d9e1"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f7e37f89acfe237d4eaf93c3b69b0f01f407a7a5d0b5a8f06ba91943ea3cf10"}, {file = "tokenizers-0.15.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a8da7533dbe66b88afd430c56a2f2ce1fd82e2681868f857da38eeb3191d7498"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:6a63a15b523d42ebc1f4028e5a568013388c2aefa4053a263e511cb10aaa02f1"}, {file = "tokenizers-0.15.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa8eb4584fc6cbe6a84d7a7864be3ed28e23e9fd2146aa8ef1814d579df91958"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2417d9e4958a6c2fbecc34c27269e74561c55d8823bf914b422e261a11fdd5fd"}, {file = "tokenizers-0.15.0.tar.gz", hash = "sha256:10c7e6e7b4cabd757da59e93f5f8d1126291d16f8b54f28510825ef56a3e5d0e"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8550974bace6210e41ab04231e06408cf99ea4279e0862c02b8d47e7c2b2828"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:194ba82129b171bcd29235a969e5859a93e491e9b0f8b2581f500f200c85cfdd"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1bfd95eef8b01e6c0805dbccc8eaf41d8c5a84f0cce72c0ab149fe76aae0bce6"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b87a15dd72f8216b03c151e3dace00c75c3fe7b0ee9643c25943f31e582f1a34"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6ac22f358a0c2a6c685be49136ce7ea7054108986ad444f567712cf274b34cd8"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e9d1f046a9b9d9a95faa103f07db5921d2c1c50f0329ebba4359350ee02b18b"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a0fd30a4b74485f6a7af89fffb5fb84d6d5f649b3e74f8d37f624cc9e9e97cf"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e45dc206b9447fa48795a1247c69a1732d890b53e2cc51ba42bc2fefa22407"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eaff56ef3e218017fa1d72007184401f04cb3a289990d2b6a0a76ce71c95f96"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b41dc107e4a4e9c95934e79b025228bbdda37d9b153d8b084160e88d5e48ad6f"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1922b8582d0c33488764bcf32e80ef6054f515369e70092729c928aae2284bc2"},
{file = "tokenizers-0.15.1.tar.gz", hash = "sha256:c0a331d6d5a3d6e97b7f99f562cee8d56797180797bc55f12070e495e717c980"},
] ]
[package.dependencies] [package.dependencies]
@@ -3276,13 +3264,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.27.0.post1" version = "0.26.0"
description = "The lightning-fast ASGI server." description = "The lightning-fast ASGI server."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "uvicorn-0.27.0.post1-py3-none-any.whl", hash = "sha256:4b85ba02b8a20429b9b205d015cbeb788a12da527f731811b643fd739ef90d5f"}, {file = "uvicorn-0.26.0-py3-none-any.whl", hash = "sha256:cdb58ef6b8188c6c174994b2b1ba2150a9a8ae7ea5fb2f1b856b94a815d6071d"},
{file = "uvicorn-0.27.0.post1.tar.gz", hash = "sha256:54898fcd80c13ff1cd28bf77b04ec9dbd8ff60c5259b499b4b12bb0917f22907"}, {file = "uvicorn-0.26.0.tar.gz", hash = "sha256:48bfd350fce3c5c57af5fb4995fded8fb50da3b4feb543eb18ad7e0d54589602"},
] ]
[package.dependencies] [package.dependencies]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.94.0" version = "1.93.3"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"
-4
View File
@@ -1,4 +0,0 @@
{
"flutterSdkVersion": "3.16.9",
"flavors": {}
}
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"flutter": "3.16.9" "flutter": "3.13.6"
} }
+1 -1
View File
@@ -55,4 +55,4 @@ default.isar.lock
libisar.so libisar.so
# FVM Version Cache # FVM Version Cache
.fvm/flutter_sdk .fvm/
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"dart.flutterSdkPath": ".fvm\\versions\\3.16.9", "dart.flutterSdkPath": ".fvm\\versions\\3.13.6",
"search.exclude": { "search.exclude": {
"**/.fvm": true "**/.fvm": true
}, },
+1 -1
View File
@@ -34,7 +34,7 @@ if (keystorePropertiesFile.exists()) {
android { android {
compileSdkVersion 34 compileSdkVersion 33
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 120, "android.injected.version.code" => 119,
"android.injected.version.name" => "1.94.0", "android.injected.version.name" => "1.93.3",
} }
) )
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')
-11
View File
@@ -142,15 +142,12 @@
"control_bottom_app_bar_archive": "Arxiu", "control_bottom_app_bar_archive": "Arxiu",
"control_bottom_app_bar_create_new_album": "Crea un àlbum nou", "control_bottom_app_bar_create_new_album": "Crea un àlbum nou",
"control_bottom_app_bar_delete": "Esborra", "control_bottom_app_bar_delete": "Esborra",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_edit_location": "Edit Location", "control_bottom_app_bar_edit_location": "Edit Location",
"control_bottom_app_bar_edit_time": "Edit Date & Time", "control_bottom_app_bar_edit_time": "Edit Date & Time",
"control_bottom_app_bar_favorite": "Preferit", "control_bottom_app_bar_favorite": "Preferit",
"control_bottom_app_bar_share": "Share", "control_bottom_app_bar_share": "Share",
"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": "Move to Trash",
"control_bottom_app_bar_unarchive": "Desarxiva", "control_bottom_app_bar_unarchive": "Desarxiva",
"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,15 +162,9 @@
"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": "These items will be permanently deleted from Immich and from your device", "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
"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": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
"delete_dialog_cancel": "Cancel·la", "delete_dialog_cancel": "Cancel·la",
"delete_dialog_ok": "Esborra", "delete_dialog_ok": "Esborra",
"delete_dialog_ok_force": "Delete Anyway",
"delete_dialog_title": "Esborra permanentment", "delete_dialog_title": "Esborra permanentment",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway",
"delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?",
"delete_shared_link_dialog_title": "Delete Shared Link", "delete_shared_link_dialog_title": "Delete Shared Link",
"description_input_hint_text": "Afegeix descripció...", "description_input_hint_text": "Afegeix descripció...",
@@ -199,7 +190,6 @@
"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": "Building the timeline", "home_page_building_timeline": "Building the 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": "Local assets in delete remote selection, skipping",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"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": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
@@ -275,7 +265,6 @@
"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": "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 Photos", "motion_photos_page_title": "Motion Photos",
+14 -25
View File
@@ -19,8 +19,8 @@
"album_thumbnail_card_shared": " · Sdíleno", "album_thumbnail_card_shared": " · Sdíleno",
"album_thumbnail_owned": "Vlastní", "album_thumbnail_owned": "Vlastní",
"album_thumbnail_shared_by": "Sdílel(a) {}", "album_thumbnail_shared_by": "Sdílel(a) {}",
"album_viewer_appbar_share_delete": "Smazat album", "album_viewer_appbar_share_delete": "Odstranit album",
"album_viewer_appbar_share_err_delete": "Nepodařilo se smazat album", "album_viewer_appbar_share_err_delete": "Nepodařilo se odstranit album",
"album_viewer_appbar_share_err_leave": "Nepodařilo se opustit album", "album_viewer_appbar_share_err_leave": "Nepodařilo se opustit album",
"album_viewer_appbar_share_err_remove": "Při odstraňování položek z alba se vyskytly problémy.", "album_viewer_appbar_share_err_remove": "Při odstraňování položek z alba se vyskytly problémy.",
"album_viewer_appbar_share_err_title": "Nepodařilo se změnit název alba", "album_viewer_appbar_share_err_title": "Nepodařilo se změnit název alba",
@@ -35,8 +35,8 @@
"app_bar_signout_dialog_title": "Odhlásit se", "app_bar_signout_dialog_title": "Odhlásit se",
"archive_page_no_archived_assets": "Nebyla nalezena žádná archivovaná média", "archive_page_no_archived_assets": "Nebyla nalezena žádná archivovaná média",
"archive_page_title": "Archív ({})", "archive_page_title": "Archív ({})",
"asset_action_delete_err_read_only": "Nelze odstranit položky pouze pro čtení, přeskakuji", "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_share_err_offline": "Nelze načíst offline položky, přeskakuji", "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_list_layout_settings_dynamic_layout_title": "Dynamické rozložení", "asset_list_layout_settings_dynamic_layout_title": "Dynamické rozložení",
"asset_list_layout_settings_group_automatically": "Automaticky", "asset_list_layout_settings_group_automatically": "Automaticky",
"asset_list_layout_settings_group_by": "Seskupit položky podle", "asset_list_layout_settings_group_by": "Seskupit položky podle",
@@ -141,16 +141,13 @@
"control_bottom_app_bar_album_info_shared": "{} položky sdílené", "control_bottom_app_bar_album_info_shared": "{} položky sdílené",
"control_bottom_app_bar_archive": "Archív", "control_bottom_app_bar_archive": "Archív",
"control_bottom_app_bar_create_new_album": "Vytvořit nové album", "control_bottom_app_bar_create_new_album": "Vytvořit nové album",
"control_bottom_app_bar_delete": "Smazat", "control_bottom_app_bar_delete": "Vymazat",
"control_bottom_app_bar_delete_from_immich": "Smazat ze serveru Immich",
"control_bottom_app_bar_delete_from_local": "Smazat ze zařízení",
"control_bottom_app_bar_edit_location": "Upravit polohu", "control_bottom_app_bar_edit_location": "Upravit polohu",
"control_bottom_app_bar_edit_time": "Upravit datum a čas", "control_bottom_app_bar_edit_time": "Upravit datum a čas",
"control_bottom_app_bar_favorite": "Oblíbené", "control_bottom_app_bar_favorite": "Oblíbené",
"control_bottom_app_bar_share": "Sdílet", "control_bottom_app_bar_share": "Sdílet",
"control_bottom_app_bar_share_to": "Sdílet v", "control_bottom_app_bar_share_to": "Sdílet v",
"control_bottom_app_bar_stack": "Zásobník", "control_bottom_app_bar_stack": "Zásobník",
"control_bottom_app_bar_trash_from_immich": "Přesunout do koše",
"control_bottom_app_bar_unarchive": "Odarchivovat", "control_bottom_app_bar_unarchive": "Odarchivovat",
"control_bottom_app_bar_unfavorite": "Zrušit oblíbení", "control_bottom_app_bar_unfavorite": "Zrušit oblíbení",
"control_bottom_app_bar_upload": "Nahrát", "control_bottom_app_bar_upload": "Nahrát",
@@ -164,16 +161,10 @@
"daily_title_text_date": "EEEE, d. MMMM", "daily_title_text_date": "EEEE, d. MMMM",
"daily_title_text_date_year": "EEEE, d. MMMM y", "daily_title_text_date_year": "EEEE, d. MMMM y",
"date_format": "EEEE, d. MMMM y • H:mm", "date_format": "EEEE, d. MMMM y • H:mm",
"delete_dialog_alert": "Tyto položky budou trvale smazány z aplikace Immich i z vašeho zařízení", "delete_dialog_alert": "Tyto položky budou trvale odstraněny z Immich i z vašeho zařízení",
"delete_dialog_alert_local": "Tyto položky budou z vašeho zařízení trvale smazány, ale budou stále k dispozici na Immich serveru",
"delete_dialog_alert_local_non_backed_up": "Některé položky nejsou zálohovány na Immich server a budou ze zařízení trvale smazány",
"delete_dialog_alert_remote": "Tyto položky budou trvale smazány z Immich serveru ",
"delete_dialog_cancel": "Zrušit", "delete_dialog_cancel": "Zrušit",
"delete_dialog_ok": "Smazat", "delete_dialog_ok": "Vymazat",
"delete_dialog_ok_force": "Přesto smazat", "delete_dialog_title": "Vymazat trvale",
"delete_dialog_title": "Smazat trvale",
"delete_local_dialog_ok_backed_up_only": "Smazat pouze zálohované",
"delete_local_dialog_ok_force": "Přesto smazat",
"delete_shared_link_dialog_content": "Opravdu chcete tento odkaz ke sdílení odstranit?", "delete_shared_link_dialog_content": "Opravdu chcete tento odkaz ke sdílení odstranit?",
"delete_shared_link_dialog_title": "Odstranit sdílený odkaz", "delete_shared_link_dialog_title": "Odstranit sdílený odkaz",
"description_input_hint_text": "Přidat popis...", "description_input_hint_text": "Přidat popis...",
@@ -198,8 +189,7 @@
"home_page_archive_err_local": "Zatím nemohu archivovat lokální média, přeskakuji", "home_page_archive_err_local": "Zatím nemohu archivovat lokální média, přeskakuji",
"home_page_archive_err_partner": "Položky partnera nelze archivovat, přeskakuji", "home_page_archive_err_partner": "Položky partnera nelze archivovat, přeskakuji",
"home_page_building_timeline": "Vytváření časové osy", "home_page_building_timeline": "Vytváření časové osy",
"home_page_delete_err_partner": "Položky partnera nelze smazat, přeskakuji", "home_page_delete_err_partner": "Položky partnera nelze odstranit, přeskakuji",
"home_page_delete_remote_err_local": "Místní položky ve vzdáleném výběru pro smazání, přeskakuji",
"home_page_favorite_err_local": "Zatím není možné zařadit lokální média mezi oblíbená, přeskakuji", "home_page_favorite_err_local": "Zatím není možné zařadit lokální média mezi oblíbená, přeskakuji",
"home_page_favorite_err_partner": "Položky partnera nelze označit jako oblíbené, přeskakuji", "home_page_favorite_err_partner": "Položky partnera nelze označit jako oblíbené, přeskakuji",
"home_page_first_time_notice": "Pokud aplikaci používáte poprvé, nezapomeňte si vybrat zálohovaná alba, aby se na časové ose mohly nacházet fotografie a videa z vybraných alb.", "home_page_first_time_notice": "Pokud aplikaci používáte poprvé, nezapomeňte si vybrat zálohovaná alba, aby se na časové ose mohly nacházet fotografie a videa z vybraných alb.",
@@ -257,7 +247,7 @@
"map_cannot_get_user_location": "Nelze zjistit polohu uživatele", "map_cannot_get_user_location": "Nelze zjistit polohu uživatele",
"map_location_dialog_cancel": "Zrušit", "map_location_dialog_cancel": "Zrušit",
"map_location_dialog_yes": "Ano", "map_location_dialog_yes": "Ano",
"map_location_picker_page_use_location": "Použít tuto polohu", "map_location_picker_page_use_location": "Použijte tuto polohu",
"map_location_service_disabled_content": "Pro zobrazení fotek z vaší aktuální polohy musí být povolena služba určování polohy. Chcete ji nyní povolit?", "map_location_service_disabled_content": "Pro zobrazení fotek z vaší aktuální polohy musí být povolena služba určování polohy. Chcete ji nyní povolit?",
"map_location_service_disabled_title": "Služba určování polohy je zakázána", "map_location_service_disabled_title": "Služba určování polohy je zakázána",
"map_no_assets_in_bounds": "Žádné fotografie v této oblasti", "map_no_assets_in_bounds": "Žádné fotografie v této oblasti",
@@ -268,19 +258,18 @@
"map_settings_date_range_option_day": "Posledních 24 hodin", "map_settings_date_range_option_day": "Posledních 24 hodin",
"map_settings_date_range_option_days": "Posledních {} dní", "map_settings_date_range_option_days": "Posledních {} dní",
"map_settings_date_range_option_year": "Poslední rok", "map_settings_date_range_option_year": "Poslední rok",
"map_settings_date_range_option_years": "Poslední {} roky", "map_settings_date_range_option_years": "Posledních {} let",
"map_settings_dialog_cancel": "Zrušit", "map_settings_dialog_cancel": "Zrušit",
"map_settings_dialog_save": "Uložit", "map_settings_dialog_save": "Uložit",
"map_settings_dialog_title": "Nastavení map", "map_settings_dialog_title": "Nastavení map",
"map_settings_include_show_archived": "Zahrnout archivované", "map_settings_include_show_archived": "Zahrnout archivované",
"map_settings_only_relative_range": "Rozsah data", "map_settings_only_relative_range": "Rozsah data",
"map_settings_only_show_favorites": "Zobrazit pouze oblíbené", "map_settings_only_show_favorites": "Zobrazit pouze oblíbené",
"map_settings_theme_settings": "Motiv mapy",
"map_zoom_to_see_photos": "Oddálit pro zobrazení fotografií", "map_zoom_to_see_photos": "Oddálit pro zobrazení fotografií",
"monthly_title_text_date_format": "LLLL y", "monthly_title_text_date_format": "LLLL y",
"motion_photos_page_title": "Pohyblivé fotky", "motion_photos_page_title": "Pohyblivé fotky",
"multiselect_grid_edit_date_time_err_read_only": "Nelze upravit datum položek pouze pro čtení, přeskakuji", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Nelze upravit polohu položek pouze pro čtení, přeskakuji", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"notification_permission_dialog_cancel": "Zrušit", "notification_permission_dialog_cancel": "Zrušit",
"notification_permission_dialog_content": "Chcete-li povolit oznámení, přejděte do nastavení a vyberte možnost povolit.", "notification_permission_dialog_content": "Chcete-li povolit oznámení, přejděte do nastavení a vyberte možnost povolit.",
"notification_permission_dialog_settings": "Nastavení", "notification_permission_dialog_settings": "Nastavení",
@@ -456,7 +445,7 @@
"trash_page_empty_trash_btn": "Vysypat koš", "trash_page_empty_trash_btn": "Vysypat koš",
"trash_page_empty_trash_dialog_content": "Chcete vyprázdnit svoje vyhozené položky? Tyto položky budou trvale odstraněny z aplikace", "trash_page_empty_trash_dialog_content": "Chcete vyprázdnit svoje vyhozené položky? Tyto položky budou trvale odstraněny z aplikace",
"trash_page_empty_trash_dialog_ok": "Ok", "trash_page_empty_trash_dialog_ok": "Ok",
"trash_page_info": "Vyhozené položky budou trvale smazány po {} dnech", "trash_page_info": "Vyhozené položky budou trvale odstraněny po {} dnech",
"trash_page_no_assets": "Žádné vyhozené položky", "trash_page_no_assets": "Žádné vyhozené položky",
"trash_page_restore": "Obnovit", "trash_page_restore": "Obnovit",
"trash_page_restore_all": "Obnovit všechny", "trash_page_restore_all": "Obnovit všechny",
-11
View File
@@ -142,15 +142,12 @@
"control_bottom_app_bar_archive": "Arkiv", "control_bottom_app_bar_archive": "Arkiv",
"control_bottom_app_bar_create_new_album": "Opret nyt album", "control_bottom_app_bar_create_new_album": "Opret nyt album",
"control_bottom_app_bar_delete": "Slet", "control_bottom_app_bar_delete": "Slet",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_edit_location": "Rediger placering", "control_bottom_app_bar_edit_location": "Rediger placering",
"control_bottom_app_bar_edit_time": "Rediger tid og dato", "control_bottom_app_bar_edit_time": "Rediger tid og dato",
"control_bottom_app_bar_favorite": "Favorit", "control_bottom_app_bar_favorite": "Favorit",
"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": "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",
@@ -165,15 +162,9 @@
"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": "Disse elementer vil blive slettet permanent fra Immich og din enhed", "delete_dialog_alert": "Disse elementer vil blive slettet permanent fra Immich og din enhed",
"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": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
"delete_dialog_cancel": "Annuller", "delete_dialog_cancel": "Annuller",
"delete_dialog_ok": "Slet", "delete_dialog_ok": "Slet",
"delete_dialog_ok_force": "Delete Anyway",
"delete_dialog_title": "Slet permanent", "delete_dialog_title": "Slet permanent",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway",
"delete_shared_link_dialog_content": "Er du sikker på, du vil slette dette delte link?", "delete_shared_link_dialog_content": "Er du sikker på, du vil slette dette delte link?",
"delete_shared_link_dialog_title": "Slet delt link", "delete_shared_link_dialog_title": "Slet delt link",
"description_input_hint_text": "Tilføj en beskrivelse...", "description_input_hint_text": "Tilføj en beskrivelse...",
@@ -199,7 +190,6 @@
"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": "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.",
@@ -275,7 +265,6 @@
"map_settings_include_show_archived": "Inkluder arkiveret", "map_settings_include_show_archived": "Inkluder arkiveret",
"map_settings_only_relative_range": "Datointerval", "map_settings_only_relative_range": "Datointerval",
"map_settings_only_show_favorites": "Vis kun favoritter", "map_settings_only_show_favorites": "Vis kun favoritter",
"map_settings_theme_settings": "Map Theme",
"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",
+80 -91
View File
@@ -1,9 +1,9 @@
{ {
"action_common_cancel": "Abbrechen", "action_common_cancel": "Cancel",
"action_common_update": "Aktualisieren", "action_common_update": "Update",
"add_to_album_bottom_sheet_added": "Zu {album} hinzugefügt", "add_to_album_bottom_sheet_added": "Zu {album} hinzugefügt",
"add_to_album_bottom_sheet_already_exists": "Bereits in {album}", "add_to_album_bottom_sheet_already_exists": "Bereits in {album}",
"advanced_settings_log_level_title": "Log-Level: {}", "advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "Manche Endgeräte laden Vorschaubilder lokaler Bilder sehr langsam. Durch diese Einstellung werden diese stattdessen direkt vom Server geladen.", "advanced_settings_prefer_remote_subtitle": "Manche Endgeräte laden Vorschaubilder lokaler Bilder sehr langsam. Durch diese Einstellung werden diese stattdessen direkt vom Server geladen.",
"advanced_settings_prefer_remote_title": "Server-Bilder bevorzugen", "advanced_settings_prefer_remote_title": "Server-Bilder bevorzugen",
"advanced_settings_self_signed_ssl_subtitle": "Verifizierung von SSL-Zertifikaten vom Server überspringen. Notwendig bei selbstsignierten Zertifikaten.", "advanced_settings_self_signed_ssl_subtitle": "Verifizierung von SSL-Zertifikaten vom Server überspringen. Notwendig bei selbstsignierten Zertifikaten.",
@@ -35,8 +35,8 @@
"app_bar_signout_dialog_title": "Abmelden", "app_bar_signout_dialog_title": "Abmelden",
"archive_page_no_archived_assets": "Keine archivierten Inhalte gefunden", "archive_page_no_archived_assets": "Keine archivierten Inhalte gefunden",
"archive_page_title": "Archiv ({})", "archive_page_title": "Archiv ({})",
"asset_action_delete_err_read_only": "Schreibgeschützten Inhalte können nicht gelöscht werden, überspringe", "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_share_err_offline": "Offline-Inhalte konnten nicht gelesen werden, überspringe", "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_list_layout_settings_dynamic_layout_title": "Dynamisches Layout", "asset_list_layout_settings_dynamic_layout_title": "Dynamisches Layout",
"asset_list_layout_settings_group_automatically": "Automatisch", "asset_list_layout_settings_group_automatically": "Automatisch",
"asset_list_layout_settings_group_by": "Gruppiere Elemente nach", "asset_list_layout_settings_group_by": "Gruppiere Elemente nach",
@@ -80,7 +80,7 @@
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos", "backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
"backup_controller_page_cancel": "Abbrechen", "backup_controller_page_cancel": "Abbrechen",
"backup_controller_page_created": "Erstellt: {}", "backup_controller_page_created": "Erstellt: {}",
"backup_controller_page_desc_backup": "Aktiviere die Sicherung, um Elemente immer automatisch auf den Server zu laden, während du die App benutzt.", "backup_controller_page_desc_backup": "Aktiviere die Sicherung um Elemente automatisch auf den Server zu laden.",
"backup_controller_page_excluded": "Ausgeschlossen: ", "backup_controller_page_excluded": "Ausgeschlossen: ",
"backup_controller_page_failed": "Fehlgeschlagen ({})", "backup_controller_page_failed": "Fehlgeschlagen ({})",
"backup_controller_page_filename": "Dateiname: {} [{}]", "backup_controller_page_filename": "Dateiname: {} [{}]",
@@ -92,14 +92,14 @@
"backup_controller_page_select": "Auswählen", "backup_controller_page_select": "Auswählen",
"backup_controller_page_server_storage": "Server Speicher", "backup_controller_page_server_storage": "Server Speicher",
"backup_controller_page_start_backup": "Sicherung starten", "backup_controller_page_start_backup": "Sicherung starten",
"backup_controller_page_status_off": "Sicherung im Vordergrund ist inaktiv", "backup_controller_page_status_off": "Sicherung ist inaktiv",
"backup_controller_page_status_on": "Sicherung im Vordergrund ist aktiv", "backup_controller_page_status_on": "Sicherung ist aktiv",
"backup_controller_page_storage_format": "{} von {} genutzt", "backup_controller_page_storage_format": "{} von {} genutzt",
"backup_controller_page_to_backup": "Zu sichernde Alben", "backup_controller_page_to_backup": "Zu sichernde Alben",
"backup_controller_page_total": "Gesamt", "backup_controller_page_total": "Gesamt",
"backup_controller_page_total_sub": "Alle Fotos und Videos", "backup_controller_page_total_sub": "Alle Fotos und Videos",
"backup_controller_page_turn_off": "Sicherung im Vordergrund ausschalten", "backup_controller_page_turn_off": "Sicherung ausschalten",
"backup_controller_page_turn_on": "Sicherung im Vordergrund einschalten", "backup_controller_page_turn_on": "Sicherung einschalten",
"backup_controller_page_uploading_file_info": "Informationen", "backup_controller_page_uploading_file_info": "Informationen",
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden", "backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
"backup_info_card_assets": "Elemente", "backup_info_card_assets": "Elemente",
@@ -111,9 +111,9 @@
"cache_settings_album_thumbnails": "Vorschaubilder der Bibliothek ({} Elemente)", "cache_settings_album_thumbnails": "Vorschaubilder der Bibliothek ({} Elemente)",
"cache_settings_clear_cache_button": "Zwischenspeicher löschen", "cache_settings_clear_cache_button": "Zwischenspeicher löschen",
"cache_settings_clear_cache_button_title": "Löscht den Zwischenspeicher der App. Dies wird die Leistungsfähigkeit der App deutlich einschränken, bis der Zwischenspeicher wieder aufgebaut wurde.", "cache_settings_clear_cache_button_title": "Löscht den Zwischenspeicher der App. Dies wird die Leistungsfähigkeit der App deutlich einschränken, bis der Zwischenspeicher wieder aufgebaut wurde.",
"cache_settings_duplicated_assets_clear_button": "LEEREN", "cache_settings_duplicated_assets_clear_button": "CLEAR",
"cache_settings_duplicated_assets_subtitle": "Inhalte, die von der App versteckt werden", "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
"cache_settings_duplicated_assets_title": "Duplikate ({})", "cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
"cache_settings_image_cache_size": "{} Bilder im Zwischenspeicher", "cache_settings_image_cache_size": "{} Bilder im Zwischenspeicher",
"cache_settings_statistics_album": "Vorschaubilder der Bibliothek", "cache_settings_statistics_album": "Vorschaubilder der Bibliothek",
"cache_settings_statistics_assets": "{} Elemente ({})", "cache_settings_statistics_assets": "{} Elemente ({})",
@@ -142,17 +142,14 @@
"control_bottom_app_bar_archive": "Archiv", "control_bottom_app_bar_archive": "Archiv",
"control_bottom_app_bar_create_new_album": "Neues Album erstellen", "control_bottom_app_bar_create_new_album": "Neues Album erstellen",
"control_bottom_app_bar_delete": "Löschen", "control_bottom_app_bar_delete": "Löschen",
"control_bottom_app_bar_delete_from_immich": "Von Immich löschen", "control_bottom_app_bar_edit_location": "Edit Location",
"control_bottom_app_bar_delete_from_local": "Vom Gerät löschen", "control_bottom_app_bar_edit_time": "Edit Date & Time",
"control_bottom_app_bar_edit_location": "Ort bearbeiten",
"control_bottom_app_bar_edit_time": "Datum und Uhrzeit bearbeiten",
"control_bottom_app_bar_favorite": "Favorit", "control_bottom_app_bar_favorite": "Favorit",
"control_bottom_app_bar_share": "Teilen", "control_bottom_app_bar_share": "Teilen",
"control_bottom_app_bar_share_to": "Teilen mit", "control_bottom_app_bar_share_to": "Teilen mit",
"control_bottom_app_bar_stack": "Stapeln", "control_bottom_app_bar_stack": "Stapeln",
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
"control_bottom_app_bar_unarchive": "Dearchivieren", "control_bottom_app_bar_unarchive": "Dearchivieren",
"control_bottom_app_bar_unfavorite": "Aus Favoriten entfernen", "control_bottom_app_bar_unfavorite": "Unfavorite",
"control_bottom_app_bar_upload": "Hochladen", "control_bottom_app_bar_upload": "Hochladen",
"create_album_page_untitled": "Unbenannt", "create_album_page_untitled": "Unbenannt",
"create_shared_album_page_create": "Erstellen", "create_shared_album_page_create": "Erstellen",
@@ -165,26 +162,20 @@
"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": "Diese Elemente werden unwiderruflich von Immich und dem Gerät entfernt", "delete_dialog_alert": "Diese Elemente werden unwiderruflich von Immich und dem Gerät entfernt",
"delete_dialog_alert_local": "Diese Inhalte werden vom Gerät gelöscht, bleiben aber auf dem Immich-Server",
"delete_dialog_alert_local_non_backed_up": "Einige Inhalte wurden nicht zu Immich gesichert und werden dauerhaft vom Gerät gelöscht",
"delete_dialog_alert_remote": "Diese Inhalte werden dauerhaft vom Immich-Server gelöscht",
"delete_dialog_cancel": "Abbrechen", "delete_dialog_cancel": "Abbrechen",
"delete_dialog_ok": "Löschen", "delete_dialog_ok": "Löschen",
"delete_dialog_ok_force": "Trotzdem löschen",
"delete_dialog_title": "Endgültig löschen", "delete_dialog_title": "Endgültig löschen",
"delete_local_dialog_ok_backed_up_only": "Nur gesicherte Inhalte löschen",
"delete_local_dialog_ok_force": "Trotzdem löschen",
"delete_shared_link_dialog_content": "Bist du sicher, dass du diesen geteilten Link löschen möchtest?", "delete_shared_link_dialog_content": "Bist du sicher, dass du diesen geteilten Link löschen möchtest?",
"delete_shared_link_dialog_title": "Geteilten Link löschen", "delete_shared_link_dialog_title": "Geteilten Link löschen",
"description_input_hint_text": "Beschreibung hinzufügen...", "description_input_hint_text": "Beschreibung hinzufügen...",
"description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log für mehr Details nachsehen.", "description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log für mehr Details nachsehen.",
"edit_date_time_dialog_date_time": "Datum und Uhrzeit", "edit_date_time_dialog_date_time": "Date and Time",
"edit_date_time_dialog_timezone": "Zeitzone", "edit_date_time_dialog_timezone": "Timezone",
"edit_location_dialog_title": "Ort bearbeiten", "edit_location_dialog_title": "Location",
"exif_bottom_sheet_description": "Beschreibung hinzufügen...", "exif_bottom_sheet_description": "Beschreibung hinzufügen...",
"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": "Add a location",
"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!",
@@ -199,7 +190,6 @@
"home_page_archive_err_partner": "Inhalte von Partnern können nicht archiviert werden", "home_page_archive_err_partner": "Inhalte von Partnern können nicht archiviert werden",
"home_page_building_timeline": "Zeitachse wird erstellt.", "home_page_building_timeline": "Zeitachse wird erstellt.",
"home_page_delete_err_partner": "Inhalte von Partnern können nicht gelöscht werden", "home_page_delete_err_partner": "Inhalte von Partnern können nicht gelöscht werden",
"home_page_delete_remote_err_local": "Lokale Inhalte in der Auswahl, überspringe",
"home_page_favorite_err_local": "Kann lokale Elemente noch nicht favorisieren, überspringe", "home_page_favorite_err_local": "Kann lokale Elemente noch nicht favorisieren, überspringe",
"home_page_favorite_err_partner": "Inhalte von Partnern können nicht favorisiert werden", "home_page_favorite_err_partner": "Inhalte von Partnern können nicht favorisiert werden",
"home_page_first_time_notice": "Wenn dies das erste Mal ist dass Du Immich nutzt, stelle bitte sicher, dass mindestens ein Album zur Sicherung ausgewählt ist, sodass die Zeitachse mit Fotos und Videos gefüllt werden kann.", "home_page_first_time_notice": "Wenn dies das erste Mal ist dass Du Immich nutzt, stelle bitte sicher, dass mindestens ein Album zur Sicherung ausgewählt ist, sodass die Zeitachse mit Fotos und Videos gefüllt werden kann.",
@@ -214,22 +204,22 @@
"library_page_favorites": "Favoriten", "library_page_favorites": "Favoriten",
"library_page_new_album": "Neues Album", "library_page_new_album": "Neues Album",
"library_page_sharing": "Teilen", "library_page_sharing": "Teilen",
"library_page_sort_asset_count": "Anzahl der Inhalte", "library_page_sort_asset_count": "Number of assets",
"library_page_sort_created": "Zuletzt erstellt", "library_page_sort_created": "Zuletzt erstellt",
"library_page_sort_last_modified": "Zuletzt bearbeitet", "library_page_sort_last_modified": "Zuletzt bearbeitet",
"library_page_sort_most_oldest_photo": "Ältestes Foto", "library_page_sort_most_oldest_photo": "Oldest photo",
"library_page_sort_most_recent_photo": "Neuestes Foto", "library_page_sort_most_recent_photo": "Neuestes Foto",
"library_page_sort_title": "Titel des Albums", "library_page_sort_title": "Titel des Albums",
"location_picker_choose_on_map": "Auf der Karte auswählen", "location_picker_choose_on_map": "Choose on map",
"location_picker_latitude": "Breitengrad", "location_picker_latitude": "Latitude",
"location_picker_latitude_error": "Gültigen Breitengrad eingeben", "location_picker_latitude_error": "Enter a valid latitude",
"location_picker_latitude_hint": "Breitengrad eingeben", "location_picker_latitude_hint": "Enter your latitude here",
"location_picker_longitude": "Längengrad", "location_picker_longitude": "Longitude",
"location_picker_longitude_error": "Gültigen Längengrad eingeben", "location_picker_longitude_error": "Enter a valid longitude",
"location_picker_longitude_hint": "Längengrad eingeben", "location_picker_longitude_hint": "Enter your longitude here",
"login_disabled": "Login ist deaktiviert", "login_disabled": "Login ist deaktiviert",
"login_form_api_exception": "API Fehler. Bitte die Serveradresse überprüfen und erneut versuchen.", "login_form_api_exception": "API Fehler. Bitte die Serveradresse überprüfen und erneut versuchen.",
"login_form_back_button_text": "Zurück", "login_form_back_button_text": "Back",
"login_form_button_text": "Anmelden", "login_form_button_text": "Anmelden",
"login_form_email_hint": "deine@email.de", "login_form_email_hint": "deine@email.de",
"login_form_endpoint_hint": "http://deine-server-ip:port/api", "login_form_endpoint_hint": "http://deine-server-ip:port/api",
@@ -252,35 +242,34 @@
"login_form_server_error": "Konnte nicht mit Server verbinden.", "login_form_server_error": "Konnte nicht mit Server verbinden.",
"login_password_changed_error": "Fehler beim Passwort ändern", "login_password_changed_error": "Fehler beim Passwort ändern",
"login_password_changed_success": "Passwort erfolgreich geändert", "login_password_changed_success": "Passwort erfolgreich geändert",
"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": "Standort konnte nicht ermittelt werden", "map_cannot_get_user_location": "Standort konnte nicht ermittelt werden",
"map_location_dialog_cancel": "Abbrechen", "map_location_dialog_cancel": "Abbrechen",
"map_location_dialog_yes": "Ja", "map_location_dialog_yes": "Ja",
"map_location_picker_page_use_location": "Aufnahmeort verwenden", "map_location_picker_page_use_location": "Use this location",
"map_location_service_disabled_content": "Ortungsdienste müssen aktiviert sein, um Inhalte am aktuellen Standort anzuzeigen. Willst du die Ortungsdienste aktivieren?", "map_location_service_disabled_content": "Ortungsdienste müssen aktiviert sein, um Inhalte am aktuellen Standort anzuzeigen. Willst du die Ortungsdienste aktivieren?",
"map_location_service_disabled_title": "Ortungsdienste deaktiviert", "map_location_service_disabled_title": "Ortungsdienste deaktiviert",
"map_no_assets_in_bounds": "Keine Fotos in dieser Gegend", "map_no_assets_in_bounds": "Keine Fotos in dieser Gegend",
"map_no_location_permission_content": "Ortungsdienste müssen aktiviert sein, um Inhalte am aktuellen Standort anzuzeigen. Willst du die Ortungsdienste aktivieren?", "map_no_location_permission_content": "Ortungsdienste müssen aktiviert sein, um Inhalte am aktuellen Standort anzuzeigen. Willst du die Ortungsdienste aktivieren?",
"map_no_location_permission_title": "Kein Zugriff auf den Standort", "map_no_location_permission_title": "Kein Zugriff auf den Standort",
"map_settings_dark_mode": "Dunkler Modus", "map_settings_dark_mode": "Dunkler Modus",
"map_settings_date_range_option_all": "Alle", "map_settings_date_range_option_all": "All",
"map_settings_date_range_option_day": "Letzte 24 Stunden", "map_settings_date_range_option_day": "Past 24 hours",
"map_settings_date_range_option_days": "Letzte {} Tage", "map_settings_date_range_option_days": "Past {} days",
"map_settings_date_range_option_year": "Letztes Jahr", "map_settings_date_range_option_year": "Past year",
"map_settings_date_range_option_years": "Letzte {} Jahre", "map_settings_date_range_option_years": "Past {} years",
"map_settings_dialog_cancel": "Abbrechen", "map_settings_dialog_cancel": "Abbrechen",
"map_settings_dialog_save": "Speichern", "map_settings_dialog_save": "Speichern",
"map_settings_dialog_title": "Karteneinstellungen", "map_settings_dialog_title": "Karteneinstellungen",
"map_settings_include_show_archived": "Archivierte anzeigen", "map_settings_include_show_archived": "Archivierte anzeigen",
"map_settings_only_relative_range": "Datumsbereich", "map_settings_only_relative_range": "Datumsbereich",
"map_settings_only_show_favorites": "Nur Favoriten anzeigen", "map_settings_only_show_favorites": "Nur Favoriten anzeigen",
"map_settings_theme_settings": "Karten-Theme",
"map_zoom_to_see_photos": "Ansicht verkleinern um Fotos zu sehen", "map_zoom_to_see_photos": "Ansicht verkleinern um Fotos zu sehen",
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Live-Fotos", "motion_photos_page_title": "Live-Fotos",
"multiselect_grid_edit_date_time_err_read_only": "Datum und Uhrzeit von schreibgeschützten Inhalten kann nicht geändert werden, überspringe", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Der Aufnahmeort von schreibgeschützten Inhalten kann nicht geändert werden, überspringe", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"notification_permission_dialog_cancel": "Abbrechen", "notification_permission_dialog_cancel": "Abbrechen",
"notification_permission_dialog_content": "Um Benachrichtigungen zu aktivieren, navigiere zu Einstellungen und klicke \"Erlauben\"", "notification_permission_dialog_content": "Um Benachrichtigungen zu aktivieren, navigiere zu Einstellungen und klicke \"Erlauben\"",
"notification_permission_dialog_settings": "Einstellungen", "notification_permission_dialog_settings": "Einstellungen",
@@ -307,18 +296,18 @@
"permission_onboarding_permission_limited": "Berechtigungen unzureichend. Um Immich das Sichern von ganzen Sammlungen zu ermöglichen, muss der Zugriff auf alle Fotos und Videos in den Einstellungen erlaubt werden.", "permission_onboarding_permission_limited": "Berechtigungen unzureichend. Um Immich das Sichern von ganzen Sammlungen zu ermöglichen, muss der Zugriff auf alle Fotos und Videos in den Einstellungen erlaubt werden.",
"permission_onboarding_request": "Immich benötigt Berechtigung um auf deine Fotos und Videos zuzugreifen.", "permission_onboarding_request": "Immich benötigt Berechtigung um auf deine Fotos und Videos zuzugreifen.",
"profile_drawer_app_logs": "Logs", "profile_drawer_app_logs": "Logs",
"profile_drawer_client_out_of_date_major": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Major-Version.", "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": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Minor-Version.", "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": "App und Server sind aktuell", "profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
"profile_drawer_documentation": "Dokumentation", "profile_drawer_documentation": "Dokumentation",
"profile_drawer_github": "GitHub", "profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "Server-Version ist veraltet. Bitte aktualisiere auf die neueste Major-Version.", "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": "Server-Version ist veraltet. Bitte aktualisiere auf die neueste Minor-Version.", "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
"profile_drawer_settings": "Einstellungen", "profile_drawer_settings": "Einstellungen",
"profile_drawer_sign_out": "Abmelden", "profile_drawer_sign_out": "Abmelden",
"profile_drawer_trash": "Papierkorb", "profile_drawer_trash": "Papierkorb",
"recently_added_page_title": "Zuletzt hinzugefügt", "recently_added_page_title": "Zuletzt hinzugefügt",
"scaffold_body_error_occurred": "Ein Fehler ist aufgetreten", "scaffold_body_error_occurred": "Error occurred",
"search_bar_hint": "Durchsuche deine Fotos", "search_bar_hint": "Durchsuche deine Fotos",
"search_page_categories": "Kategorien", "search_page_categories": "Kategorien",
"search_page_favorites": "Favoriten", "search_page_favorites": "Favoriten",
@@ -326,13 +315,13 @@
"search_page_no_objects": "Keine Objektinformationen verfügbar", "search_page_no_objects": "Keine Objektinformationen verfügbar",
"search_page_no_places": "Keine Informationen über Orte verfügbar", "search_page_no_places": "Keine Informationen über Orte verfügbar",
"search_page_people": "Personen", "search_page_people": "Personen",
"search_page_person_add_name_dialog_cancel": "Abbrechen", "search_page_person_add_name_dialog_cancel": "Cancel",
"search_page_person_add_name_dialog_hint": "Name", "search_page_person_add_name_dialog_hint": "Name",
"search_page_person_add_name_dialog_save": "Speichern", "search_page_person_add_name_dialog_save": "Save",
"search_page_person_add_name_dialog_title": "Name hinzufügen", "search_page_person_add_name_dialog_title": "Add a name",
"search_page_person_add_name_subtitle": "Name für die Suchfunktion hinzufügen", "search_page_person_add_name_subtitle": "Find them fast by name with search",
"search_page_person_add_name_title": "Name hinzufügen", "search_page_person_add_name_title": "Add a name",
"search_page_person_edit_name": "Name bearbeiten", "search_page_person_edit_name": "Edit name",
"search_page_places": "Orte", "search_page_places": "Orte",
"search_page_recently_added": "Zuletzt hinzugefügt", "search_page_recently_added": "Zuletzt hinzugefügt",
"search_page_screenshots": "Bildschirmfotos", "search_page_screenshots": "Bildschirmfotos",
@@ -341,7 +330,7 @@
"search_page_videos": "Videos", "search_page_videos": "Videos",
"search_page_view_all_button": "Alle anzeigen", "search_page_view_all_button": "Alle anzeigen",
"search_page_your_activity": "Deine Aktivität", "search_page_your_activity": "Deine Aktivität",
"search_page_your_map": "Deine Karte", "search_page_your_map": "Your Map",
"search_result_page_new_search_hint": "Neue Suche", "search_result_page_new_search_hint": "Neue Suche",
"search_suggestion_list_smart_search_hint_1": "Intelligente Suche ist standardmäßig aktiviert; um nach Metadaten zu suchen, folgenden Syntax benutzen: ", "search_suggestion_list_smart_search_hint_1": "Intelligente Suche ist standardmäßig aktiviert; um nach Metadaten zu suchen, folgenden Syntax benutzen: ",
"search_suggestion_list_smart_search_hint_2": "m:dein-suchbegriff", "search_suggestion_list_smart_search_hint_2": "m:dein-suchbegriff",
@@ -381,17 +370,17 @@
"shared_album_activity_remove_title": "Aktivität entfernen", "shared_album_activity_remove_title": "Aktivität entfernen",
"shared_album_activity_setting_subtitle": "Lass andere reagieren.", "shared_album_activity_setting_subtitle": "Lass andere reagieren.",
"shared_album_activity_setting_title": "Kommentare & Likes", "shared_album_activity_setting_title": "Kommentare & Likes",
"shared_album_section_people_action_error": "Fehler beim Verlassen oder Entfernen aus dem Album", "shared_album_section_people_action_error": "Error leaving/removing from album",
"shared_album_section_people_action_leave": "Album verlassen", "shared_album_section_people_action_leave": "Remove user from album",
"shared_album_section_people_action_remove_user": "Benutzer von Album entfernen", "shared_album_section_people_action_remove_user": "Remove user from album",
"shared_album_section_people_owner_label": "Eigentümer", "shared_album_section_people_owner_label": "Owner",
"shared_album_section_people_title": "PERSONEN", "shared_album_section_people_title": "PEOPLE",
"share_dialog_preparing": "Vorbereiten...", "share_dialog_preparing": "Vorbereiten...",
"shared_link_app_bar_title": "Geteilte Links", "shared_link_app_bar_title": "Geteilte Links",
"shared_link_clipboard_copied_massage": "Link kopiert", "shared_link_clipboard_copied_massage": "Copied to clipboard",
"shared_link_clipboard_text": "Link: {}\nPasswort: {}", "shared_link_clipboard_text": "Link: {}\nPassword: {}",
"shared_link_create_app_bar_title": "Link zum Teilen erstellen", "shared_link_create_app_bar_title": "Link zum Teilen erstellen",
"shared_link_create_error": "Fehler beim Erstellen der Linkfreigabe", "shared_link_create_error": "Error while creating shared link",
"shared_link_create_info": "Alle, die über den Link verfügen, können die Fotos sehen", "shared_link_create_info": "Alle, die über den Link verfügen, können die Fotos sehen",
"shared_link_create_submit_button": "Link erstellen", "shared_link_create_submit_button": "Link erstellen",
"shared_link_edit_allow_download": "Jeder darf herunterladen", "shared_link_edit_allow_download": "Jeder darf herunterladen",
@@ -401,32 +390,32 @@
"shared_link_edit_description": "Beschreibung", "shared_link_edit_description": "Beschreibung",
"shared_link_edit_description_hint": "Beschreibung eingeben", "shared_link_edit_description_hint": "Beschreibung eingeben",
"shared_link_edit_expire_after": "Erlischt nach", "shared_link_edit_expire_after": "Erlischt nach",
"shared_link_edit_expire_after_option_day": "1 Tag", "shared_link_edit_expire_after_option_day": "1 day",
"shared_link_edit_expire_after_option_days": "{} Tage", "shared_link_edit_expire_after_option_days": "{} days",
"shared_link_edit_expire_after_option_hour": "1 Stunde", "shared_link_edit_expire_after_option_hour": "1 hour",
"shared_link_edit_expire_after_option_hours": "{} Stunden", "shared_link_edit_expire_after_option_hours": "{} hours",
"shared_link_edit_expire_after_option_minute": "1 Minute", "shared_link_edit_expire_after_option_minute": "1 minute",
"shared_link_edit_expire_after_option_minutes": "{} Minuten", "shared_link_edit_expire_after_option_minutes": "{} minutes",
"shared_link_edit_expire_after_option_never": "Nie", "shared_link_edit_expire_after_option_never": "Never",
"shared_link_edit_password": "Passwort", "shared_link_edit_password": "Passwort",
"shared_link_edit_password_hint": "Passwort eingeben", "shared_link_edit_password_hint": "Passwort eingeben",
"shared_link_edit_show_meta": "Metadaten anzeigen", "shared_link_edit_show_meta": "Metadaten anzeigen",
"shared_link_edit_submit_button": "Link aktualisieren", "shared_link_edit_submit_button": "Link aktualisieren",
"shared_link_empty": "Du hast keine geteilten Links", "shared_link_empty": "Du hast keine geteilten Links",
"shared_link_error_server_url_fetch": "Fehler beim Ermitteln der Server-URL", "shared_link_error_server_url_fetch": "Cannot fetch the server url",
"shared_link_expired": "Abgelaufen", "shared_link_expired": "Expired",
"shared_link_expires_day": "Verfällt in {} Tag", "shared_link_expires_day": "Expires in {} day",
"shared_link_expires_days": "Verfällt in {} Tagen", "shared_link_expires_days": "Expires in {} days",
"shared_link_expires_hour": "Verfällt in {} Stunde", "shared_link_expires_hour": "Expires in {} hour",
"shared_link_expires_hours": "Verfällt in {} Stunden", "shared_link_expires_hours": "Expires in {} hours",
"shared_link_expires_minute": "Verfällt in {} Minute", "shared_link_expires_minute": "Expires in {} minute",
"shared_link_expires_minutes": "Verfällt in {} Minuten", "shared_link_expires_minutes": "Expires in {} minutes",
"shared_link_expires_never": "Läuft nie ab", "shared_link_expires_never": "Expires ∞",
"shared_link_expires_second": "Verfällt in {} Sekunde", "shared_link_expires_second": "Expires in {} second",
"shared_link_expires_seconds": "Verfällt in {} Sekunden", "shared_link_expires_seconds": "Expires in {} seconds",
"shared_link_info_chip_download": "Herunterladen", "shared_link_info_chip_download": "Download",
"shared_link_info_chip_metadata": "EXIF", "shared_link_info_chip_metadata": "EXIF",
"shared_link_info_chip_upload": "Hochladen", "shared_link_info_chip_upload": "Upload",
"shared_link_manage_links": "Geteilte Links verwalten", "shared_link_manage_links": "Geteilte Links verwalten",
"share_done": "Fertig", "share_done": "Fertig",
"share_invite": "Zum Album einladen", "share_invite": "Zum Album einladen",
+6 -8
View File
@@ -141,16 +141,16 @@
"control_bottom_app_bar_album_info_shared": "{} items · Shared", "control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_archive": "Archive", "control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "Create new album", "control_bottom_app_bar_create_new_album": "Create new album",
"control_bottom_app_bar_delete": "Delete",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich", "control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
"control_bottom_app_bar_delete_from_local": "Delete from device", "control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_delete": "Remove Everywhere",
"control_bottom_app_bar_edit_location": "Edit Location", "control_bottom_app_bar_edit_location": "Edit Location",
"control_bottom_app_bar_edit_time": "Edit Date & Time", "control_bottom_app_bar_edit_time": "Edit Date & Time",
"control_bottom_app_bar_favorite": "Favorite", "control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Share", "control_bottom_app_bar_share": "Share",
"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": "Move to Trash",
"control_bottom_app_bar_unarchive": "Unarchive", "control_bottom_app_bar_unarchive": "Unarchive",
"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,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": "These items will be permanently deleted from Immich and from your device", "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
"delete_dialog_alert_remote": "These items will be permanently deleted from the 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": "These items will be permanently removed from your device but still be available on the Immich server",
"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_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": "These items will be permanently deleted from the Immich server",
"delete_dialog_cancel": "Cancel", "delete_dialog_cancel": "Cancel",
"delete_dialog_ok": "Delete", "delete_dialog_ok": "Delete",
"delete_dialog_ok_force": "Delete Anyway", "delete_dialog_ok_force": "Delete Anyway",
"delete_dialog_title": "Delete Permanently",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway", "delete_local_dialog_ok_force": "Delete Anyway",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_dialog_title": "Delete Permanently",
"delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?",
"delete_shared_link_dialog_title": "Delete Shared Link", "delete_shared_link_dialog_title": "Delete Shared Link",
"description_input_hint_text": "Add description...", "description_input_hint_text": "Add description...",
@@ -263,7 +263,7 @@
"map_no_assets_in_bounds": "No photos in this area", "map_no_assets_in_bounds": "No photos in this area",
"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_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": "Dark mode", "map_settings_theme_settings": "Map Theme",
"map_settings_date_range_option_all": "All", "map_settings_date_range_option_all": "All",
"map_settings_date_range_option_day": "Past 24 hours", "map_settings_date_range_option_day": "Past 24 hours",
"map_settings_date_range_option_days": "Past {} days", "map_settings_date_range_option_days": "Past {} days",
@@ -275,7 +275,6 @@
"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": "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 Photos", "motion_photos_page_title": "Motion Photos",
@@ -389,7 +388,6 @@
"share_dialog_preparing": "Preparing...", "share_dialog_preparing": "Preparing...",
"shared_link_app_bar_title": "Shared Links", "shared_link_app_bar_title": "Shared Links",
"shared_link_clipboard_copied_massage": "Copied to clipboard", "shared_link_clipboard_copied_massage": "Copied to clipboard",
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
"shared_link_create_app_bar_title": "Create link to share", "shared_link_create_app_bar_title": "Create link to share",
"shared_link_create_error": "Error while creating shared link", "shared_link_create_error": "Error while creating shared link",
"shared_link_create_info": "Let anyone with the link see the selected photo(s)", "shared_link_create_info": "Let anyone with the link see the selected photo(s)",
+19 -30
View File
@@ -3,7 +3,7 @@
"action_common_update": "Update", "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": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de recursos encontrados el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.", "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de recursos encontrados el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.",
"advanced_settings_prefer_remote_title": "Preferir imágenes remotas", "advanced_settings_prefer_remote_title": "Preferir imágenes remotas",
"advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados", "advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados",
@@ -111,7 +111,7 @@
"cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} archivos)", "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} archivos)",
"cache_settings_clear_cache_button": "Borrar caché", "cache_settings_clear_cache_button": "Borrar caché",
"cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.", "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.",
"cache_settings_duplicated_assets_clear_button": "LIMPIAR", "cache_settings_duplicated_assets_clear_button": "CLEAR",
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})", "cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
"cache_settings_image_cache_size": "Tamaño de la caché de imágenes ({} archivos)", "cache_settings_image_cache_size": "Tamaño de la caché de imágenes ({} archivos)",
@@ -142,15 +142,12 @@
"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": "Delete from Immich", "control_bottom_app_bar_edit_location": "Edit Location",
"control_bottom_app_bar_delete_from_local": "Delete from device", "control_bottom_app_bar_edit_time": "Edit Date & Time",
"control_bottom_app_bar_edit_location": "Editar ubicación",
"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": "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,15 +162,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": "These items will be permanently removed from your device but still be available on the Immich server",
"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": "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_title": "Eliminar Permanentemente", "delete_dialog_title": "Eliminar Permanentemente",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway",
"delete_shared_link_dialog_content": "Estás seguro que quieres eliminar este enlace compartido", "delete_shared_link_dialog_content": "Estás seguro que quieres eliminar este enlace compartido",
"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...",
@@ -184,7 +175,7 @@
"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": "Add a location",
"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",
@@ -199,7 +190,6 @@
"home_page_archive_err_partner": "No se pueden archivar activos de un compañero, omitiendo", "home_page_archive_err_partner": "No se pueden archivar activos de un compañero, omitiendo",
"home_page_building_timeline": "Construyendo la línea de tiempo", "home_page_building_timeline": "Construyendo la línea de tiempo",
"home_page_delete_err_partner": "No se pueden eliminar activos de un compañero, omitiendo", "home_page_delete_err_partner": "No se pueden eliminar activos de un compañero, omitiendo",
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
"home_page_favorite_err_local": "Aún no se pueden archivar recursos locales, omitiendo", "home_page_favorite_err_local": "Aún no se pueden archivar recursos locales, omitiendo",
"home_page_favorite_err_partner": "Aún no se pueden marcar recursos de compañeros como favoritos, omitiendo", "home_page_favorite_err_partner": "Aún no se pueden marcar recursos de compañeros como favoritos, omitiendo",
"home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.", "home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.",
@@ -214,10 +204,10 @@
"library_page_favorites": "Favoritos", "library_page_favorites": "Favoritos",
"library_page_new_album": "Nuevo álbum", "library_page_new_album": "Nuevo álbum",
"library_page_sharing": "Compartiendo", "library_page_sharing": "Compartiendo",
"library_page_sort_asset_count": "Número de archivos", "library_page_sort_asset_count": "Number of assets",
"library_page_sort_created": "Creado más recientemente", "library_page_sort_created": "Creado más recientemente",
"library_page_sort_last_modified": "Última modificación", "library_page_sort_last_modified": "Última modificación",
"library_page_sort_most_oldest_photo": "Foto más antigua", "library_page_sort_most_oldest_photo": "Oldest photo",
"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": "Choose on map", "location_picker_choose_on_map": "Choose on map",
@@ -229,7 +219,7 @@
"location_picker_longitude_hint": "Enter your longitude here", "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": "Back",
"login_form_button_text": "Iniciar Sesión", "login_form_button_text": "Iniciar Sesión",
"login_form_email_hint": "tucorreo@correo.com", "login_form_email_hint": "tucorreo@correo.com",
"login_form_endpoint_hint": "http://tu-ip-de-servidor:puerto/api", "login_form_endpoint_hint": "http://tu-ip-de-servidor:puerto/api",
@@ -264,7 +254,7 @@
"map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?", "map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?",
"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": "All",
"map_settings_date_range_option_day": "Past 24 hours", "map_settings_date_range_option_day": "Past 24 hours",
"map_settings_date_range_option_days": "Past {} days", "map_settings_date_range_option_days": "Past {} days",
"map_settings_date_range_option_year": "Past year", "map_settings_date_range_option_year": "Past year",
@@ -275,7 +265,6 @@
"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": "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",
@@ -307,13 +296,13 @@
"permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.", "permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.",
"permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.", "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.",
"profile_drawer_app_logs": "Registros", "profile_drawer_app_logs": "Registros",
"profile_drawer_client_out_of_date_major": "La app de móvil está desactualizada. Por favor actualiza a la última versión principal", "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": "La app de móvil está desactualizada. Por favor actualiza a la última versión menor", "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": "El Cliente y el Servidor están actualizados", "profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados",
"profile_drawer_documentation": "Documentación", "profile_drawer_documentation": "Documentación",
"profile_drawer_github": "GitHub", "profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "El servidor está desactualizado. Por favor actualiza a la última versión principal", "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": "El servidor está desactualizado. Por favor actualiza a la última versión menor", "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
"profile_drawer_settings": "Configuración", "profile_drawer_settings": "Configuración",
"profile_drawer_sign_out": "Cerrar Sesión", "profile_drawer_sign_out": "Cerrar Sesión",
"profile_drawer_trash": "Papelera", "profile_drawer_trash": "Papelera",
@@ -326,10 +315,10 @@
"search_page_no_objects": "No hay información de objetos disponibles", "search_page_no_objects": "No hay información de objetos disponibles",
"search_page_no_places": "No hay información de lugares disponibles", "search_page_no_places": "No hay información de lugares disponibles",
"search_page_people": "Personas", "search_page_people": "Personas",
"search_page_person_add_name_dialog_cancel": "Cancelar", "search_page_person_add_name_dialog_cancel": "Cancel",
"search_page_person_add_name_dialog_hint": "Nombre", "search_page_person_add_name_dialog_hint": "Name",
"search_page_person_add_name_dialog_save": "Guardar", "search_page_person_add_name_dialog_save": "Save",
"search_page_person_add_name_dialog_title": "Añadir nombre", "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": "Add a name", "search_page_person_add_name_title": "Add a name",
"search_page_person_edit_name": "Edit name", "search_page_person_edit_name": "Edit name",
@@ -420,10 +409,10 @@
"shared_link_expires_hour": "Expires in {} hour", "shared_link_expires_hour": "Expires in {} hour",
"shared_link_expires_hours": "Expires in {} hours", "shared_link_expires_hours": "Expires in {} hours",
"shared_link_expires_minute": "Expires in {} minute", "shared_link_expires_minute": "Expires in {} minute",
"shared_link_expires_minutes": "Caduca en {} minutos", "shared_link_expires_minutes": "Expires in {} minutes",
"shared_link_expires_never": "Expires ∞", "shared_link_expires_never": "Expires ∞",
"shared_link_expires_second": "Expires in {} second", "shared_link_expires_second": "Expires in {} second",
"shared_link_expires_seconds": "Caduca en {} segundos", "shared_link_expires_seconds": "Expires in {} seconds",
"shared_link_info_chip_download": "Download", "shared_link_info_chip_download": "Download",
"shared_link_info_chip_metadata": "EXIF", "shared_link_info_chip_metadata": "EXIF",
"shared_link_info_chip_upload": "Upload", "shared_link_info_chip_upload": "Upload",

Some files were not shown because too many files have changed in this diff Show More